Merge pull request 'Telemetry' (#24) from telemetry into main

Reviewed-on: #24
This commit was merged in pull request #24.
This commit is contained in:
2026-07-21 21:51:09 +00:00
13 changed files with 922 additions and 151 deletions
+11 -1
View File
@@ -48,4 +48,14 @@ Contributions, feedback and feature requests are always welcome!
## 📄 License ## 📄 License
MIT License 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.
+252 -123
View File
@@ -574,13 +574,22 @@ dependencies = [
"generic-array", "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]] [[package]]
name = "block2" name = "block2"
version = "0.6.2" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
] ]
[[package]] [[package]]
@@ -1119,6 +1128,19 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 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]] [[package]]
name = "core-graphics" name = "core-graphics"
version = "0.25.0" version = "0.25.0"
@@ -1592,9 +1614,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2 0.6.2",
"libc", "libc",
"objc2", "objc2 0.6.4",
] ]
[[package]] [[package]]
@@ -2538,8 +2560,10 @@ dependencies = [
"commit_ai", "commit_ai",
"keyring", "keyring",
"log", "log",
"reqwest 0.12.28",
"serde", "serde",
"serde_json", "serde_json",
"sysinfo 0.38.3",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-aptabase", "tauri-plugin-aptabase",
@@ -3884,7 +3908,7 @@ dependencies = [
"statrs", "statrs",
"strum 0.27.2", "strum 0.27.2",
"symphonia", "symphonia",
"sysinfo", "sysinfo 0.36.1",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokenizers 0.21.4", "tokenizers 0.21.4",
"tokio", "tokio",
@@ -4014,10 +4038,10 @@ dependencies = [
"dpi", "dpi",
"gtk", "gtk",
"keyboard-types", "keyboard-types",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"png 0.18.1", "png 0.18.1",
"serde", "serde",
@@ -4311,6 +4335,22 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 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]] [[package]]
name = "objc2" name = "objc2"
version = "0.6.4" version = "0.6.4"
@@ -4323,92 +4363,80 @@ dependencies = [
[[package]] [[package]]
name = "objc2-app-kit" name = "objc2-app-kit"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2 0.6.2",
"objc2", "objc2 0.6.4",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-cloud-kit" name = "objc2-cloud-kit"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-core-data" name = "objc2-core-data"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-core-foundation" name = "objc2-core-foundation"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"dispatch2", "dispatch2",
"objc2", "objc2 0.6.4",
] ]
[[package]] [[package]]
name = "objc2-core-graphics" name = "objc2-core-graphics"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"dispatch2", "dispatch2",
"objc2", "objc2 0.6.4",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-surface", "objc2-io-surface",
] ]
[[package]] [[package]]
name = "objc2-core-image" name = "objc2-core-image"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-core-location" name = "objc2-core-location"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" checksum = "ac0f75792558aa9d618443bbb5db7426a7a0b6fddf96903f86ef9ad02e135740"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
]
[[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",
] ]
[[package]] [[package]]
@@ -4428,22 +4456,34 @@ dependencies = [
[[package]] [[package]]
name = "objc2-foundation" name = "objc2-foundation"
version = "0.3.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2 0.5.1",
"libc", "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", "objc2-core-foundation",
] ]
[[package]] [[package]]
name = "objc2-io-kit" name = "objc2-io-kit"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a"
dependencies = [ dependencies = [
"libc", "libc",
"objc2-core-foundation", "objc2-core-foundation",
@@ -4451,82 +4491,106 @@ dependencies = [
[[package]] [[package]]
name = "objc2-io-surface" name = "objc2-io-surface"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"objc2", "objc2 0.6.4",
"objc2-core-foundation", "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]] [[package]]
name = "objc2-osa-kit" name = "objc2-osa-kit"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-quartz-core" name = "objc2-quartz-core"
version = "0.3.2" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [ dependencies = [
"bitflags 2.13.0", "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-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-ui-kit" name = "objc2-ui-kit"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2 0.6.2",
"objc2", "objc2 0.6.4",
"objc2-cloud-kit", "objc2-cloud-kit",
"objc2-core-data", "objc2-core-data",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-core-image", "objc2-core-image",
"objc2-core-location", "objc2-core-location",
"objc2-core-text", "objc2-foundation 0.3.1",
"objc2-foundation", "objc2-quartz-core 0.3.1",
"objc2-quartz-core",
"objc2-user-notifications", "objc2-user-notifications",
] ]
[[package]] [[package]]
name = "objc2-user-notifications" name = "objc2-user-notifications"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" checksum = "2a3f5ec77a81d9e0c5a0b32159b0cb143d7086165e79708351e02bf37dfc65cd"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
name = "objc2-web-kit" name = "objc2-web-kit"
version = "0.3.2" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2 0.6.2",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
] ]
[[package]] [[package]]
@@ -4664,8 +4728,8 @@ dependencies = [
"android_system_properties", "android_system_properties",
"log", "log",
"nix 0.31.3", "nix 0.31.3",
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
"objc2-ui-kit", "objc2-ui-kit",
"serde", "serde",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -4677,8 +4741,8 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
"objc2-foundation", "objc2-foundation 0.3.1",
"objc2-osa-kit", "objc2-osa-kit",
"serde", "serde",
"serde_json", "serde_json",
@@ -5540,17 +5604,17 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [ dependencies = [
"block2", "block2 0.6.2",
"dispatch2", "dispatch2",
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
"gtk-sys", "gtk-sys",
"js-sys", "js-sys",
"log", "log",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
"raw-window-handle", "raw-window-handle",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@@ -6358,24 +6422,24 @@ dependencies = [
[[package]] [[package]]
name = "softbuffer" name = "softbuffer"
version = "0.4.8" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"cfg_aliases",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"js-sys", "js-sys",
"ndk", "log",
"objc2", "objc2 0.5.2",
"objc2-core-foundation", "objc2-foundation 0.2.2",
"objc2-core-graphics", "objc2-quartz-core 0.2.2",
"objc2-foundation",
"objc2-quartz-core",
"raw-window-handle", "raw-window-handle",
"redox_syscall", "redox_syscall",
"tracing",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -6772,7 +6836,21 @@ dependencies = [
"ntapi", "ntapi",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-io-kit", "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]] [[package]]
@@ -6816,9 +6894,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"block2", "block2 0.6.2",
"core-foundation 0.10.1", "core-foundation 0.10.1",
"core-graphics", "core-graphics 0.25.0",
"crossbeam-channel", "crossbeam-channel",
"dbus", "dbus",
"dispatch2", "dispatch2",
@@ -6832,9 +6910,9 @@ dependencies = [
"log", "log",
"ndk", "ndk",
"ndk-sys", "ndk-sys",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation 0.3.1",
"objc2-ui-kit", "objc2-ui-kit",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
@@ -6843,7 +6921,7 @@ dependencies = [
"tao-macros", "tao-macros",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
@@ -6899,9 +6977,9 @@ dependencies = [
"log", "log",
"mime", "mime",
"muda", "muda",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation", "objc2-foundation 0.3.1",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"percent-encoding", "percent-encoding",
@@ -6925,7 +7003,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"window-vibrancy", "window-vibrancy",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@@ -7054,7 +7132,7 @@ dependencies = [
"dunce", "dunce",
"glob", "glob",
"log", "log",
"objc2-foundation", "objc2-foundation 0.3.1",
"percent-encoding", "percent-encoding",
"schemars 0.8.22", "schemars 0.8.22",
"serde", "serde",
@@ -7127,7 +7205,7 @@ dependencies = [
"gtk", "gtk",
"http", "http",
"jni 0.21.1", "jni 0.21.1",
"objc2", "objc2 0.6.4",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"raw-window-handle", "raw-window-handle",
@@ -7138,7 +7216,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@@ -7151,7 +7229,7 @@ dependencies = [
"http", "http",
"jni 0.21.1", "jni 0.21.1",
"log", "log",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
@@ -7163,7 +7241,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"wry", "wry",
] ]
@@ -7811,11 +7889,11 @@ dependencies = [
"dirs", "dirs",
"libappindicator", "libappindicator",
"muda", "muda",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
"objc2-foundation", "objc2-foundation 0.3.1",
"once_cell", "once_cell",
"png 0.18.1", "png 0.18.1",
"serde", "serde",
@@ -8389,7 +8467,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [ dependencies = [
"webview2-com-macros", "webview2-com-macros",
"webview2-com-sys", "webview2-com-sys",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
@@ -8413,7 +8491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
@@ -8476,10 +8554,10 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [ dependencies = [
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
"raw-window-handle", "raw-window-handle",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"windows-version", "windows-version",
@@ -8491,11 +8569,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [ dependencies = [
"windows-collections", "windows-collections 0.2.0",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-future", "windows-future 0.2.1",
"windows-link 0.1.3", "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]] [[package]]
@@ -8507,6 +8597,15 @@ dependencies = [
"windows-core 0.61.2", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -8541,7 +8640,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-link 0.1.3", "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]] [[package]]
@@ -8588,6 +8698,16 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.6.1" version = "0.6.1"
@@ -8761,6 +8881,15 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "windows-version" name = "windows-version"
version = "0.1.7" version = "0.1.7"
@@ -9003,7 +9132,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"block2", "block2 0.6.2",
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
"dirs", "dirs",
@@ -9017,10 +9146,10 @@ dependencies = [
"jni 0.21.1", "jni 0.21.1",
"libc", "libc",
"ndk", "ndk",
"objc2", "objc2 0.6.4",
"objc2-app-kit", "objc2-app-kit",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-foundation", "objc2-foundation 0.3.1",
"objc2-ui-kit", "objc2-ui-kit",
"objc2-web-kit", "objc2-web-kit",
"once_cell", "once_cell",
@@ -9034,7 +9163,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
+2
View File
@@ -22,6 +22,8 @@ keyring = { version = "3", features = ["apple-native", "windows-native", "async-
commit_ai = { path = "crates/commit_ai" } commit_ai = { path = "crates/commit_ai" }
tokio = "1.52.3" tokio = "1.52.3"
log = "0.4" 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] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }
+7 -1
View File
@@ -2,6 +2,7 @@
mod badge; mod badge;
mod git; mod git;
mod telemetry;
use badge::set_sync_badge; use badge::set_sync_badge;
use git::{ use git::{
@@ -24,6 +25,7 @@ use git::{
unstage_files, update_remote, unstage_files, update_remote,
}; };
use tauri::Manager; use tauri::Manager;
use telemetry::{emit_frontend_log, emit_frontend_span, set_telemetry_enabled};
struct ConsoleLogger; struct ConsoleLogger;
@@ -78,6 +80,7 @@ fn close_splashscreen(app: tauri::AppHandle) -> Result<(), String> {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
init_console_logging(); init_console_logging();
telemetry::init();
if let Some(result) = run_sequence_editor_if_requested() { if let Some(result) = run_sequence_editor_if_requested() {
if let Err(error) = result { if let Err(error) = result {
eprintln!("{error}"); eprintln!("{error}");
@@ -193,7 +196,10 @@ async fn main() {
cred_save, cred_save,
cred_delete, cred_delete,
set_sync_badge, set_sync_badge,
close_splashscreen close_splashscreen,
set_telemetry_enabled,
emit_frontend_log,
emit_frontend_span
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
+363
View File
@@ -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<SyncSender<TelemetrySignal>> = 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<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrontendLog {
level: String,
message: String,
event_name: Option<String>,
}
#[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::<TelemetrySignal>(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<TelemetrySignal>) {
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));
}
}
+80 -21
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount, tick } from "svelte"; import { onDestroy, onMount, tick } from "svelte";
import { getVersion } from "@tauri-apps/api/app"; import { getVersion } from "@tauri-apps/api/app";
import { invoke } from "@tauri-apps/api/core";
import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater"; import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte"; import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
@@ -160,6 +159,7 @@
summarizeGitError, summarizeGitError,
} from "./lib/credentials"; } from "./lib/credentials";
import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics"; import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics";
import { setTelemetryEnabled, tracedInvoke } from "./lib/telemetry";
type UpdateToastState = "available" | "downloading" | "installed" | "error"; type UpdateToastState = "available" | "downloading" | "installed" | "error";
type AppView = "management" | "repository"; type AppView = "management" | "repository";
@@ -191,6 +191,14 @@
directoryName: string; directoryName: string;
} }
interface ErrorAutoHideState {
timer?: ReturnType<typeof setTimeout>;
message: string;
remaining: number;
startedAt: number;
clearIfCurrent: (message: string) => void;
}
const OPEN_REPOS_KEY = "gitlite.openRepos.v1"; const OPEN_REPOS_KEY = "gitlite.openRepos.v1";
const RECENT_REPOS_KEY = "gitlite.recentRepos.v1"; const RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1"; const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
@@ -364,7 +372,7 @@
let updateCheckInFlight = false; let updateCheckInFlight = false;
let updateDownloadTotal = 0; let updateDownloadTotal = 0;
let updateDownloadedBytes = 0; let updateDownloadedBytes = 0;
let errorAutoHideTimers: Partial<Record<string, ReturnType<typeof setTimeout>>> = {}; let errorAutoHideStates: Partial<Record<string, ErrorAutoHideState>> = {};
let commitPanelHeight = loadCommitPanelHeight(); let commitPanelHeight = loadCommitPanelHeight();
let resizingCommitPanel = false; let resizingCommitPanel = false;
let resizeStartY = 0; let resizeStartY = 0;
@@ -467,8 +475,8 @@
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer); if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
if (commitAiPollTimer) clearInterval(commitAiPollTimer); if (commitAiPollTimer) clearInterval(commitAiPollTimer);
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer); if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
Object.values(errorAutoHideTimers).forEach((timer) => { Object.values(errorAutoHideStates).forEach((state) => {
if (timer) clearTimeout(timer); if (state?.timer) clearTimeout(state.timer);
}); });
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId); if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
}); });
@@ -503,7 +511,7 @@
async function closeStartupSplashscreen() { async function closeStartupSplashscreen() {
try { try {
await invoke("close_splashscreen"); await tracedInvoke("close_splashscreen");
} catch { } catch {
// Browser preview and failed startup paths should keep working without Tauri. // Browser preview and failed startup paths should keep working without Tauri.
} }
@@ -815,6 +823,7 @@
function initAnalytics() { function initAnalytics() {
analyticsSettings = loadAnalyticsSettings(); analyticsSettings = loadAnalyticsSettings();
setTelemetryEnabled(analyticsSettings.noticeSeen && analyticsSettings.enabled);
if (!analyticsSettings.noticeSeen) { if (!analyticsSettings.noticeSeen) {
analyticsNoticeOpen = true; analyticsNoticeOpen = true;
return; return;
@@ -831,6 +840,7 @@
analyticsSettings = { enabled, noticeSeen: true }; analyticsSettings = { enabled, noticeSeen: true };
persistAnalyticsSettings(analyticsSettings); persistAnalyticsSettings(analyticsSettings);
analyticsNoticeOpen = false; analyticsNoticeOpen = false;
setTelemetryEnabled(enabled);
trackEvent("app_started", { first_run: 1 }); trackEvent("app_started", { first_run: 1 });
} }
@@ -900,6 +910,7 @@
persistThemePreference(nextTheme); persistThemePreference(nextTheme);
persistLanguagePreference(nextLanguage); persistLanguagePreference(nextLanguage);
persistStoredBoolean(AUTO_REFRESH_ENABLED_KEY, nextAutoRefresh); persistStoredBoolean(AUTO_REFRESH_ENABLED_KEY, nextAutoRefresh);
setTelemetryEnabled(next.enabled);
appSettingsOpen = false; appSettingsOpen = false;
if (nextAutoRefresh && !autoRefreshWasEnabled) void autoRefreshTick(); if (nextAutoRefresh && !autoRefreshWasEnabled) void autoRefreshTick();
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage, auto_refresh: nextAutoRefresh ? 1 : 0 }); if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage, auto_refresh: nextAutoRefresh ? 1 : 0 });
@@ -1107,18 +1118,39 @@
message: string, message: string,
clearIfCurrent: (message: string) => void, clearIfCurrent: (message: string) => void,
) { ) {
const existing = errorAutoHideTimers[key]; const existing = errorAutoHideStates[key];
if (existing) { if (existing?.timer) {
clearTimeout(existing); clearTimeout(existing.timer);
delete errorAutoHideTimers[key];
} }
delete errorAutoHideStates[key];
if (!message) return; if (!message) return;
errorAutoHideTimers[key] = setTimeout(() => { errorAutoHideStates[key] = {
clearIfCurrent(message); message,
delete errorAutoHideTimers[key]; remaining: ERROR_AUTO_HIDE_MS,
}, ERROR_AUTO_HIDE_MS); startedAt: Date.now(),
clearIfCurrent,
};
resumeAutoHideError(key);
}
function pauseAutoHideError(key: string) {
const state = errorAutoHideStates[key];
if (!state?.timer) return;
clearTimeout(state.timer);
state.timer = undefined;
state.remaining = Math.max(0, state.remaining - (Date.now() - state.startedAt));
}
function resumeAutoHideError(key: string) {
const state = errorAutoHideStates[key];
if (!state || state.timer) return;
state.startedAt = Date.now();
state.timer = setTimeout(() => {
state.clearIfCurrent(state.message);
delete errorAutoHideStates[key];
}, state.remaining);
} }
function repoKey(path: string): string { function repoKey(path: string): string {
@@ -1746,9 +1778,11 @@
} }
function errorToMessage(error: unknown): string { function errorToMessage(error: unknown): string {
if (error instanceof Error) return error.message; let message: string;
if (typeof error === "string") return error; if (error instanceof Error) message = error.message;
try { return JSON.stringify(error) ?? "Unknown error"; } catch { return "Unknown error"; } else if (typeof error === "string") message = error;
else try { message = JSON.stringify(error) ?? "Unknown error"; } catch { message = "Unknown error"; }
return message;
} }
function isNonFastForwardPushError(message: string): boolean { function isNonFastForwardPushError(message: string): boolean {
@@ -3793,14 +3827,39 @@
{/if} {/if}
{/if} {/if}
<!-- Status notices --> <!-- Global error toast -->
{#if errorMessage} {#if errorMessage}
<section class="notice error" role="alert"> {#key errorMessage}
<AlertCircle size={17} aria-hidden="true" /> <div class="error-toast-region" aria-live="assertive" aria-atomic="true">
<span>{errorMessage}</span> <section
</section> class="error-toast"
role="alert"
onmouseenter={() => pauseAutoHideError("errorMessage")}
onmouseleave={() => resumeAutoHideError("errorMessage")}
>
<div class="error-toast-icon">
<AlertCircle size={20} aria-hidden="true" />
</div>
<div class="error-toast-content">
<strong>{appLanguage === "de" ? "Etwas ist schiefgelaufen" : "Something went wrong"}</strong>
<span>{errorMessage}</span>
</div>
<button
class="error-toast-close"
type="button"
onclick={() => { errorMessage = ""; }}
title={appLanguage === "de" ? "Schließen" : "Close"}
aria-label={appLanguage === "de" ? "Fehlermeldung schließen" : "Dismiss error"}
>
<X size={15} aria-hidden="true" />
</button>
<div class="error-toast-timeout" aria-hidden="true"></div>
</section>
</div>
{/key}
{/if} {/if}
<!-- Status notices -->
{#if operation && operation !== "Opening repository" && !hasRepository} {#if operation && operation !== "Opening repository" && !hasRepository}
<section class="notice busy" aria-live="polite"> <section class="notice busy" aria-live="polite">
<LoaderCircle class="spin" size={17} aria-hidden="true" /> <LoaderCircle class="spin" size={17} aria-hidden="true" />
+124
View File
@@ -1222,6 +1222,113 @@
color: #eadfff; color: #eadfff;
} }
/* --- Global error toast --- */
.error-toast-region {
position: fixed;
top: calc(var(--app-titlebar-height, 40px) + 14px);
left: 50%;
z-index: 90;
width: min(560px, calc(100vw - 32px));
pointer-events: none;
transform: translateX(-50%);
}
.error-toast {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: start;
gap: 11px;
width: 100%;
padding: 13px 13px 14px;
overflow: hidden;
border: 1px solid rgba(232,96,96,0.46);
border-radius: 13px;
color: var(--color-ink);
background:
linear-gradient(135deg, rgba(232,96,96,0.18), rgba(100,108,255,0.08)),
rgba(12,13,24,0.96);
box-shadow: 0 18px 54px rgba(0,0,0,0.46), 0 0 0 1px rgba(255,255,255,0.035) inset;
backdrop-filter: blur(18px);
pointer-events: auto;
animation: error-toast-in 180ms cubic-bezier(.2,.8,.2,1) both;
}
.error-toast-icon {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border: 1px solid rgba(232,96,96,0.28);
border-radius: 10px;
color: #ff9b9b;
background: rgba(232,96,96,0.12);
}
.error-toast-content {
display: grid;
gap: 3px;
min-width: 0;
padding-top: 1px;
}
.error-toast-content strong {
color: #fff;
font-size: 13px;
font-weight: 800;
line-height: 1.25;
}
.error-toast-content span {
color: var(--color-ink-muted);
font-size: 12.5px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.error-toast-close {
width: 28px;
min-width: 28px;
min-height: 28px;
padding: 0;
border-color: rgba(255,255,255,0.1);
border-radius: 8px;
color: var(--color-ink-dim);
background: rgba(255,255,255,0.04);
}
.error-toast-close:hover:not(:disabled) {
border-color: rgba(255,255,255,0.2);
color: #fff;
background: rgba(255,255,255,0.1);
}
.error-toast-timeout {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, #ff7878, #ef9a9a);
transform-origin: left;
animation: error-toast-timeout 6s linear both;
}
.error-toast:hover .error-toast-timeout {
animation-play-state: paused;
}
@keyframes error-toast-in {
from { opacity: 0; transform: translateY(-10px) scale(.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes error-toast-timeout {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
/* --- Update toast --- */ /* --- Update toast --- */
.update-toast { .update-toast {
@@ -5306,6 +5413,23 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
background: rgba(210, 55, 75, 0.08); background: rgba(210, 55, 75, 0.08);
} }
:root[data-theme="light"] .error-toast {
border-color: rgba(190,48,66,0.28);
background:
linear-gradient(135deg, rgba(210,55,75,0.11), rgba(49,95,214,0.05)),
rgba(255,255,255,0.96);
box-shadow: 0 18px 48px rgba(28,44,74,0.18), 0 0 0 1px rgba(255,255,255,0.8) inset;
}
:root[data-theme="light"] .error-toast-icon {
color: #b33445;
background: rgba(210,55,75,0.09);
}
:root[data-theme="light"] .error-toast-content strong {
color: #24314a;
}
:root[data-theme="light"] .notice.busy { :root[data-theme="light"] .notice.busy {
color: #315fd6; color: #315fd6;
} }
+3
View File
@@ -1,8 +1,11 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { telemetryLog } from "./telemetry";
export type AnalyticsEventProperties = Record<string, string | number>; export type AnalyticsEventProperties = Record<string, string | number>;
export function trackAnalyticsEvent(name: string, props?: AnalyticsEventProperties) { export function trackAnalyticsEvent(name: string, props?: AnalyticsEventProperties) {
const details = props && Object.keys(props).length > 0 ? ` ${JSON.stringify(props)}` : "";
telemetryLog("info", `${name}${details}`, `product.${name}`);
void invoke("plugin:aptabase|track_event", { void invoke("plugin:aptabase|track_event", {
name, name,
props: props && Object.keys(props).length > 0 ? props : undefined, props: props && Object.keys(props).length > 0 ? props : undefined,
@@ -30,10 +30,10 @@
</div> </div>
<div class="analytics-notice-copy"> <div class="analytics-notice-copy">
<p> <p>
Gitty can send anonymous usage events to Aptabase so crashes, rough edges, and commonly used workflows are easier to improve. Gitty can send anonymous usage events and technical error logs to Aptabase and the self-hosted SigNoz service so crashes, rough edges, and commonly used workflows are easier to improve.
</p> </p>
<p> <p>
Events do not include repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, or source code. Telemetry does not include repository paths, remote URLs, branch names, commit messages, diffs, file names, credentials, or source code.
</p> </p>
</div> </div>
@@ -47,7 +47,7 @@
</div> </div>
<footer class="dialog-footer analytics-notice-footer"> <footer class="dialog-footer analytics-notice-footer">
<span class="dialog-footer-info">Privacy-friendly, optional, and limited to product usage events.</span> <span class="dialog-footer-info">Privacy-friendly, optional, and limited to product usage and technical errors.</span>
<button class="btn-primary" type="button" onclick={() => onContinue(allowAnalytics)}> <button class="btn-primary" type="button" onclick={() => onContinue(allowAnalytics)}>
<Check size={16} aria-hidden="true" /> <Check size={16} aria-hidden="true" />
Continue Continue
+1 -1
View File
@@ -124,7 +124,7 @@
<label class="settings-toggle-row"> <label class="settings-toggle-row">
<input type="checkbox" bind:checked={analyticsEnabled} /> <input type="checkbox" bind:checked={analyticsEnabled} />
<span> <span>
<strong>{isGerman ? "Anonyme Aptabase-Ereignisse erlauben" : "Allow anonymous Aptabase events"}</strong> <strong>{isGerman ? "Anonyme Analytics und Fehlerberichte erlauben" : "Allow anonymous analytics and error reports"}</strong>
<small>{isGerman ? "Es werden keine Repository-Pfade, Remotes, Branches, Commit-Nachrichten, Dateinamen, Diffs, Zugangsdaten oder Code übertragen." : "No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent."}</small> <small>{isGerman ? "Es werden keine Repository-Pfade, Remotes, Branches, Commit-Nachrichten, Dateinamen, Diffs, Zugangsdaten oder Code übertragen." : "No repository paths, remotes, branches, commit messages, file names, diffs, credentials, or code are sent."}</small>
</span> </span>
</label> </label>
+1 -1
View File
@@ -1,4 +1,4 @@
import { invoke } from "@tauri-apps/api/core"; import { tracedInvoke as invoke } from "./telemetry";
import type { import type {
AiReviewResult, AiReviewResult,
+72
View File
@@ -0,0 +1,72 @@
import { invoke } from "@tauri-apps/api/core";
type TelemetryLevel = "info" | "warn" | "error";
const MAX_MESSAGE_LENGTH = 2_048;
let telemetryEnabled = false;
let configuration: Promise<unknown> = Promise.resolve();
// Telemetry must never contain repository contents or identifying local data.
function sanitize(message: string): string {
return message
.replace(/https?:\/\/\S+/gi, "[url]")
.replace(/(?:[A-Za-z]:\\|\/)(?:[^\s<>:"|?*]+[\\/])*[^\s<>:"|?*]*/g, "[path]")
.replace(/(token|password|secret|authorization|credential)\s*[:=]\s*\S+/gi, "$1=[redacted]")
.replace(/[\w.+-]+@[\w.-]+\.[A-Za-z]{2,}/g, "[email]")
.slice(0, MAX_MESSAGE_LENGTH);
}
export function setTelemetryEnabled(enabled: boolean) {
telemetryEnabled = enabled;
configuration = invoke("set_telemetry_enabled", { enabled }).catch(() => {});
}
export function telemetryLog(level: TelemetryLevel, message: string, eventName?: string) {
if (!telemetryEnabled) return;
void configuration.then(() => invoke("emit_frontend_log", {
log: {
level,
message: sanitize(message),
eventName,
},
}))
.catch(() => {
// Observability must never interrupt the Git workflow.
});
}
function randomHex(bytes: number): string {
const values = new Uint8Array(bytes);
crypto.getRandomValues(values);
return Array.from(values, (value) => value.toString(16).padStart(2, "0")).join("");
}
export async function tracedInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
if (!telemetryEnabled) return invoke<T>(command, args);
const startedAtMs = Date.now();
const started = performance.now();
const traceId = randomHex(16);
const spanId = randomHex(8);
try {
const result = await invoke<T>(command, args);
void configuration.then(() => invoke("emit_frontend_span", {
span: { name: command, traceId, spanId, startedAtMs, durationMs: performance.now() - started, success: true },
})).catch(() => {});
return result;
} catch (error) {
void configuration.then(() => invoke("emit_frontend_span", {
span: { name: command, traceId, spanId, startedAtMs, durationMs: performance.now() - started, success: false },
})).catch(() => {});
throw error;
}
}
export function installGlobalErrorTelemetry() {
window.addEventListener("error", (event) => {
telemetryLog("error", event.message || "Unhandled frontend error", "frontend.unhandled_error");
});
window.addEventListener("unhandledrejection", (event) => {
const message = event.reason instanceof Error ? event.reason.message : String(event.reason ?? "Unhandled promise rejection");
telemetryLog("error", message, "frontend.unhandled_rejection");
});
}
+3
View File
@@ -2,10 +2,13 @@ import { mount } from "svelte";
import App from "./App.svelte"; import App from "./App.svelte";
import "./app.css"; import "./app.css";
import { installGlobalErrorTelemetry } from "./lib/telemetry";
const THEME_KEY = "gitlite.theme.v1"; const THEME_KEY = "gitlite.theme.v1";
const target = document.getElementById("app"); const target = document.getElementById("app");
installGlobalErrorTelemetry();
if (!target) { if (!target) {
throw new Error("App target element was not found."); throw new Error("App target element was not found.");
} }