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:
@@ -48,4 +48,14 @@ Contributions, feedback and feature requests are always welcome!
|
||||
|
||||
## 📄 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.
|
||||
|
||||
Generated
+252
-123
@@ -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",
|
||||
|
||||
@@ -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 = [] }
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
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";
|
||||
@@ -160,6 +159,7 @@
|
||||
summarizeGitError,
|
||||
} from "./lib/credentials";
|
||||
import { trackAnalyticsEvent, type AnalyticsEventProperties } from "./lib/analytics";
|
||||
import { setTelemetryEnabled, tracedInvoke } from "./lib/telemetry";
|
||||
|
||||
type UpdateToastState = "available" | "downloading" | "installed" | "error";
|
||||
type AppView = "management" | "repository";
|
||||
@@ -191,6 +191,14 @@
|
||||
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 RECENT_REPOS_KEY = "gitlite.recentRepos.v1";
|
||||
const FAVORITE_REPOS_KEY = "gitlite.favoriteRepos.v1";
|
||||
@@ -364,7 +372,7 @@
|
||||
let updateCheckInFlight = false;
|
||||
let updateDownloadTotal = 0;
|
||||
let updateDownloadedBytes = 0;
|
||||
let errorAutoHideTimers: Partial<Record<string, ReturnType<typeof setTimeout>>> = {};
|
||||
let errorAutoHideStates: Partial<Record<string, ErrorAutoHideState>> = {};
|
||||
let commitPanelHeight = loadCommitPanelHeight();
|
||||
let resizingCommitPanel = false;
|
||||
let resizeStartY = 0;
|
||||
@@ -467,8 +475,8 @@
|
||||
if (backgroundFetchTimer) clearInterval(backgroundFetchTimer);
|
||||
if (commitAiPollTimer) clearInterval(commitAiPollTimer);
|
||||
if (cloneDialogErrorTimer) clearTimeout(cloneDialogErrorTimer);
|
||||
Object.values(errorAutoHideTimers).forEach((timer) => {
|
||||
if (timer) clearTimeout(timer);
|
||||
Object.values(errorAutoHideStates).forEach((state) => {
|
||||
if (state?.timer) clearTimeout(state.timer);
|
||||
});
|
||||
if (activeFileHistoryRequestId) void cancelFileHistory(activeFileHistoryRequestId);
|
||||
});
|
||||
@@ -503,7 +511,7 @@
|
||||
|
||||
async function closeStartupSplashscreen() {
|
||||
try {
|
||||
await invoke("close_splashscreen");
|
||||
await tracedInvoke("close_splashscreen");
|
||||
} catch {
|
||||
// Browser preview and failed startup paths should keep working without Tauri.
|
||||
}
|
||||
@@ -815,6 +823,7 @@
|
||||
|
||||
function initAnalytics() {
|
||||
analyticsSettings = loadAnalyticsSettings();
|
||||
setTelemetryEnabled(analyticsSettings.noticeSeen && analyticsSettings.enabled);
|
||||
if (!analyticsSettings.noticeSeen) {
|
||||
analyticsNoticeOpen = true;
|
||||
return;
|
||||
@@ -831,6 +840,7 @@
|
||||
analyticsSettings = { enabled, noticeSeen: true };
|
||||
persistAnalyticsSettings(analyticsSettings);
|
||||
analyticsNoticeOpen = false;
|
||||
setTelemetryEnabled(enabled);
|
||||
trackEvent("app_started", { first_run: 1 });
|
||||
}
|
||||
|
||||
@@ -900,6 +910,7 @@
|
||||
persistThemePreference(nextTheme);
|
||||
persistLanguagePreference(nextLanguage);
|
||||
persistStoredBoolean(AUTO_REFRESH_ENABLED_KEY, nextAutoRefresh);
|
||||
setTelemetryEnabled(next.enabled);
|
||||
appSettingsOpen = false;
|
||||
if (nextAutoRefresh && !autoRefreshWasEnabled) void autoRefreshTick();
|
||||
if (next.enabled) trackEvent("settings_saved", { analytics_enabled: 1, theme: nextTheme, language: nextLanguage, auto_refresh: nextAutoRefresh ? 1 : 0 });
|
||||
@@ -1107,18 +1118,39 @@
|
||||
message: string,
|
||||
clearIfCurrent: (message: string) => void,
|
||||
) {
|
||||
const existing = errorAutoHideTimers[key];
|
||||
if (existing) {
|
||||
clearTimeout(existing);
|
||||
delete errorAutoHideTimers[key];
|
||||
const existing = errorAutoHideStates[key];
|
||||
if (existing?.timer) {
|
||||
clearTimeout(existing.timer);
|
||||
}
|
||||
delete errorAutoHideStates[key];
|
||||
|
||||
if (!message) return;
|
||||
|
||||
errorAutoHideTimers[key] = setTimeout(() => {
|
||||
clearIfCurrent(message);
|
||||
delete errorAutoHideTimers[key];
|
||||
}, ERROR_AUTO_HIDE_MS);
|
||||
errorAutoHideStates[key] = {
|
||||
message,
|
||||
remaining: 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 {
|
||||
@@ -1746,9 +1778,11 @@
|
||||
}
|
||||
|
||||
function errorToMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string") return error;
|
||||
try { return JSON.stringify(error) ?? "Unknown error"; } catch { return "Unknown error"; }
|
||||
let message: string;
|
||||
if (error instanceof Error) message = error.message;
|
||||
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 {
|
||||
@@ -3793,14 +3827,39 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Status notices -->
|
||||
<!-- Global error toast -->
|
||||
{#if errorMessage}
|
||||
<section class="notice error" role="alert">
|
||||
<AlertCircle size={17} aria-hidden="true" />
|
||||
<span>{errorMessage}</span>
|
||||
</section>
|
||||
{#key errorMessage}
|
||||
<div class="error-toast-region" aria-live="assertive" aria-atomic="true">
|
||||
<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}
|
||||
|
||||
<!-- Status notices -->
|
||||
{#if operation && operation !== "Opening repository" && !hasRepository}
|
||||
<section class="notice busy" aria-live="polite">
|
||||
<LoaderCircle class="spin" size={17} aria-hidden="true" />
|
||||
|
||||
+124
@@ -1222,6 +1222,113 @@
|
||||
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 {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
: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 {
|
||||
color: #315fd6;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { telemetryLog } from "./telemetry";
|
||||
|
||||
export type AnalyticsEventProperties = Record<string, string | number>;
|
||||
|
||||
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", {
|
||||
name,
|
||||
props: props && Object.keys(props).length > 0 ? props : undefined,
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
</div>
|
||||
<div class="analytics-notice-copy">
|
||||
<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>
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</div>
|
||||
|
||||
<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)}>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
Continue
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<label class="settings-toggle-row">
|
||||
<input type="checkbox" bind:checked={analyticsEnabled} />
|
||||
<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>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { tracedInvoke as invoke } from "./telemetry";
|
||||
|
||||
import type {
|
||||
AiReviewResult,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
@@ -2,10 +2,13 @@ import { mount } from "svelte";
|
||||
|
||||
import App from "./App.svelte";
|
||||
import "./app.css";
|
||||
import { installGlobalErrorTelemetry } from "./lib/telemetry";
|
||||
|
||||
const THEME_KEY = "gitlite.theme.v1";
|
||||
const target = document.getElementById("app");
|
||||
|
||||
installGlobalErrorTelemetry();
|
||||
|
||||
if (!target) {
|
||||
throw new Error("App target element was not found.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user