From 906b55b63ea536833f56ec64ac6db620070ffbd5 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:18:50 -0700 Subject: [PATCH] feat(observability): add Sentry error reporting to Rust core (#131) * chore(workflows): comment out Windows smoke tests in installer and release workflows * feat(observability): integrate Sentry for error reporting and add configuration support - Added Sentry integration for error reporting in the application, initializing it in the main function. - Introduced a new environment variable `OPENHUMAN_SENTRY_DSN` to configure Sentry DSN. - Updated the observability configuration schema to include a field for Sentry DSN. - Enhanced logging to include Sentry event filtering based on log levels. - Implemented secret scrubbing to protect sensitive information in error reports. * feat(analytics): add support for anonymized analytics settings - Introduced new environment variable `OPENHUMAN_ANALYTICS_ENABLED` to enable or disable anonymized analytics and crash reports. - Updated the PrivacyPanel component to sync analytics consent with the core configuration. - Added new functions to handle analytics settings updates and retrieval in the Tauri backend. - Enhanced observability configuration to include analytics settings, defaulting to enabled. - Updated relevant schemas and handlers for analytics settings in the backend. * refactor(tauriCommands): improve formatting and structure of analytics settings function - Reformatted the `openhumanUpdateAnalyticsSettings` function for better readability by adjusting the parameter structure. - Enhanced the output formatting in the `handle_get_analytics_settings` function for improved clarity in the JSON response. * feat(analytics): sync analytics consent to core RPC and improve anonymization copy Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .env.example | 8 + .github/workflows/installer-smoke.yml | 18 +- .github/workflows/release.yml | 8 +- Cargo.lock | 575 +++++++++++++++++- Cargo.toml | 1 + .../settings/panels/PrivacyPanel.tsx | 20 +- .../pages/onboarding/steps/AnalyticsStep.tsx | 33 +- app/src/utils/tauriCommands.ts | 23 + src/core/logging.rs | 23 +- src/main.rs | 62 ++ src/openhuman/config/ops.rs | 30 + src/openhuman/config/schema/load.rs | 16 + src/openhuman/config/schema/observability.rs | 15 + src/openhuman/config/schemas.rs | 60 ++ 14 files changed, 856 insertions(+), 36 deletions(-) diff --git a/.env.example b/.env.example index f2bb43a74..9e71f27b0 100644 --- a/.env.example +++ b/.env.example @@ -111,6 +111,14 @@ OLLAMA_BIN= # [optional] Override skills registry URL SKILLS_REGISTRY_URL= +# --------------------------------------------------------------------------- +# Error Reporting (Sentry) +# --------------------------------------------------------------------------- +# [optional] Sentry DSN for Rust core error reporting (no PII is sent) +OPENHUMAN_SENTRY_DSN= +# [optional] Default: true — set to false to disable anonymized analytics & crash reports +OPENHUMAN_ANALYTICS_ENABLED=true + # --------------------------------------------------------------------------- # Logging # --------------------------------------------------------------------------- diff --git a/.github/workflows/installer-smoke.yml b/.github/workflows/installer-smoke.yml index c24898b59..ac95bc949 100644 --- a/.github/workflows/installer-smoke.yml +++ b/.github/workflows/installer-smoke.yml @@ -23,13 +23,13 @@ jobs: - name: Run installer dry-run run: bash scripts/install.sh --dry-run --verbose - smoke-windows: - name: Smoke install.ps1 (windows-latest) - runs-on: windows-latest - steps: - - name: Checkout - uses: actions/checkout@v4 + # smoke-windows: + # name: Smoke install.ps1 (windows-latest) + # runs-on: windows-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v4 - - name: Run installer dry-run - shell: pwsh - run: ./scripts/install.ps1 -DryRun + # - name: Run installer dry-run + # shell: pwsh + # run: ./scripts/install.ps1 -DryRun diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b70d513c5..926461442 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,10 +221,10 @@ jobs: args: --target x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu artifact_suffix: ubuntu - - platform: windows-latest - args: --target x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc - artifact_suffix: windows + # - platform: windows-latest + # args: --target x86_64-pc-windows-msvc + # target: x86_64-pc-windows-msvc + # artifact_suffix: windows env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/Cargo.lock b/Cargo.lock index 370541fb6..c244627e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -453,6 +462,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.13.1" @@ -594,6 +618,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bs58" version = "0.5.1" @@ -686,6 +719,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cff-parser" version = "0.1.0" @@ -863,6 +902,16 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "compact_str" version = "0.9.0" @@ -1289,6 +1338,16 @@ dependencies = [ "deadpool-runtime", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + [[package]] name = "decancer" version = "3.3.3" @@ -1528,6 +1587,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2410,7 +2479,7 @@ dependencies = [ "log", "native-tls", "rand 0.9.2", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.18", @@ -3074,6 +3143,50 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -3518,7 +3631,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "reqwest", + "reqwest 0.12.28", "ruma", "serde", "serde_html_form", @@ -4170,7 +4283,7 @@ dependencies = [ "getrandom 0.2.17", "http 1.4.0", "rand 0.8.5", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "serde_path_to_error", @@ -4179,6 +4292,36 @@ dependencies = [ "url", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -4186,6 +4329,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +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.11.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -4197,6 +4429,37 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "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-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "object" version = "0.37.3" @@ -4308,7 +4571,7 @@ dependencies = [ "prost 0.14.3", "rand 0.9.2", "regex", - "reqwest", + "reqwest 0.12.28", "ring", "rppal", "rquickjs", @@ -4317,6 +4580,7 @@ dependencies = [ "rustls-pki-types", "rustyline", "schemars", + "sentry", "serde", "serde-big-array", "serde_json", @@ -4417,7 +4681,7 @@ dependencies = [ "bytes", "http 1.4.0", "opentelemetry", - "reqwest", + "reqwest 0.12.28", ] [[package]] @@ -4432,7 +4696,7 @@ dependencies = [ "opentelemetry-proto", "opentelemetry_sdk", "prost 0.14.3", - "reqwest", + "reqwest 0.12.28", "thiserror 2.0.18", ] @@ -4489,6 +4753,22 @@ version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06503bb33f294c5f1ba484011e053bfa6ae227074bdb841e9863492dc5960d4b" +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix 0.30.1", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -5124,6 +5404,7 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ + "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -5468,6 +5749,46 @@ dependencies = [ "webpki-roots 1.0.6", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -5728,6 +6049,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.2" @@ -5807,6 +6134,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.10" @@ -5873,6 +6227,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -5965,6 +6328,103 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "sentry" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb25f439f97d26fea01d717fa626167ceffcd981addaa670001e70505b72acbb" +dependencies = [ + "cfg_aliases", + "httpdate", + "reqwest 0.13.2", + "rustls", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-panic", + "sentry-tracing", + "tokio", + "ureq 3.3.0", +] + +[[package]] +name = "sentry-backtrace" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a8c2c1bd5c1f735e84f28b48e7d72efcaafc362b7541bc8253e60e8fcdffc6" +dependencies = [ + "backtrace", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b88a90baa654d7f0e1f4b667f6b434293d9f72c71bef16b197c76af5b7d5803" +dependencies = [ + "hostname", + "libc", + "os_info", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac170a5bba8bec6e3339c90432569d89641fa7a3d3e4f44987d24f0762e6adf" +dependencies = [ + "rand 0.9.2", + "sentry-types", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "sentry-panic" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6127d3d304ba5ce0409401e85aae538e303a569f8dbb031bf64f9ba0f7174346" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-tracing" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27701acc51e68db5281802b709010395bfcbcb128b1d0a4e5873680d3b47ff0c" +dependencies = [ + "bitflags 2.11.0", + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56780cb5597d676bf22e6c11d1f062eb4def46390ea3bfb047bcbcf7dfd19bdb" +dependencies = [ + "debugid", + "hex", + "rand 0.9.2", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", + "url", + "uuid", +] + [[package]] name = "serde" version = "1.0.228" @@ -7255,6 +7715,15 @@ dependencies = [ "web-time", ] +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + [[package]] name = "unescaper" version = "0.1.8" @@ -7741,6 +8210,16 @@ dependencies = [ "wa-rs-core", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -7965,6 +8444,15 @@ dependencies = [ "url", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" @@ -8024,6 +8512,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -8153,6 +8650,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -8198,6 +8704,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -8246,6 +8767,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -8264,6 +8791,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8282,6 +8815,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8312,6 +8851,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -8330,6 +8875,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -8348,6 +8899,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -8366,6 +8923,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/Cargo.toml b/Cargo.toml index 0db368f60..003568010 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,7 @@ tower = { version = "0.5", default-features = false } opentelemetry = { version = "0.31", default-features = false, features = ["trace", "metrics"] } opentelemetry_sdk = { version = "0.31", default-features = false, features = ["trace", "metrics"] } opentelemetry-otlp = { version = "0.31", default-features = false, features = ["trace", "metrics", "http-proto", "reqwest-client", "reqwest-rustls-webpki-roots"] } +sentry = { version = "0.47.0", default-features = false, features = ["backtrace", "contexts", "panic", "tracing", "reqwest", "rustls"] } tokio-stream = { version = "0.1.18", features = ["full"] } url = "2" socketioxide = { version = "0.15", features = ["extensions"] } diff --git a/app/src/components/settings/panels/PrivacyPanel.tsx b/app/src/components/settings/panels/PrivacyPanel.tsx index 88cff3c9b..a04c77094 100644 --- a/app/src/components/settings/panels/PrivacyPanel.tsx +++ b/app/src/components/settings/panels/PrivacyPanel.tsx @@ -1,6 +1,7 @@ import { syncAnalyticsConsent } from '../../../services/analytics'; import { setAnalyticsForUser } from '../../../store/authSlice'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { isTauri, openhumanUpdateAnalyticsSettings } from '../../../utils/tauriCommands'; import SettingsHeader from '../components/SettingsHeader'; import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; @@ -19,6 +20,13 @@ const PrivacyPanel = () => { const newValue = !analyticsEnabled; dispatch(setAnalyticsForUser({ userId: user._id, enabled: newValue })); syncAnalyticsConsent(newValue); + + // Sync to core config so the Rust process also respects the setting + if (isTauri()) { + openhumanUpdateAnalyticsSettings({ enabled: newValue }).catch(() => { + /* best-effort */ + }); + } }; return ( @@ -30,7 +38,7 @@ const PrivacyPanel = () => { {/* Analytics Section */}

- Analytics + Anonymized Analytics

@@ -38,7 +46,8 @@ const PrivacyPanel = () => {

Share Anonymized Usage Data

Help improve OpenHuman by sharing anonymous crash reports and usage analytics. - No personal data, messages, or wallet information is ever collected. + All data is fully anonymized — no personal data, messages, wallet keys, or + session information is ever collected.

diff --git a/app/src/pages/onboarding/steps/AnalyticsStep.tsx b/app/src/pages/onboarding/steps/AnalyticsStep.tsx index dcb45f6f2..62f257e70 100644 --- a/app/src/pages/onboarding/steps/AnalyticsStep.tsx +++ b/app/src/pages/onboarding/steps/AnalyticsStep.tsx @@ -1,5 +1,7 @@ import { useState } from 'react'; +import { isTauri, openhumanUpdateAnalyticsSettings } from '../../../utils/tauriCommands'; + interface AnalyticsStepProps { onNext: (analyticsEnabled: boolean) => void; } @@ -7,13 +9,26 @@ interface AnalyticsStepProps { const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => { const [selectedOption, setSelectedOption] = useState('shareAnalytics'); + const handleContinue = () => { + const enabled = selectedOption === 'shareAnalytics'; + + // Sync to core config so the Rust process also respects the setting + if (isTauri()) { + openhumanUpdateAnalyticsSettings({ enabled }).catch(() => { + /* best-effort */ + }); + } + + onNext(enabled); + }; + return (
-

Analytics

+

Anonymized Analytics

- We collect anonymized usage data to help us improve the app for you and for others. You - can choose to skip this. + We collect fully anonymized usage data to help improve the app. No personal data, + messages, or wallet keys are ever collected. You can change this anytime in Settings.

@@ -41,8 +56,8 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {

Share Anonymized Usage Data

- Share anonymized usage data to help us improve features and performance of the app. - This helps us improve the app for you and for others. + Share anonymized crash reports and usage analytics to help us improve features and + performance. All data is fully anonymized.

@@ -71,8 +86,8 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {

Don't Collect Anything

- We won't collect any usage analytics, ensuring total anonymity. Keep all your data - completely private. + We won't collect any usage analytics or crash reports. Keep all your data completely + private.

@@ -91,14 +106,14 @@ const AnalyticsStep = ({ onNext }: AnalyticsStepProps) => {

You can change this setting anytime

- Your privacy preferences can be updated in your account settings + Your privacy preferences can be updated in Settings > Privacy & Security

diff --git a/app/src/utils/tauriCommands.ts b/app/src/utils/tauriCommands.ts index c0e460b70..a0ad48a77 100644 --- a/app/src/utils/tauriCommands.ts +++ b/app/src/utils/tauriCommands.ts @@ -1065,6 +1065,29 @@ export async function openhumanUpdateScreenIntelligenceSettings( }); } +export async function openhumanUpdateAnalyticsSettings(update: { + enabled?: boolean; +}): Promise> { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + return await callCoreRpc>({ + method: 'openhuman.update_analytics_settings', + params: update, + }); +} + +export async function openhumanGetAnalyticsSettings(): Promise< + CommandResponse<{ enabled: boolean }> +> { + if (!isTauri()) { + throw new Error('Not running in Tauri'); + } + return await callCoreRpc>({ + method: 'openhuman.get_analytics_settings', + }); +} + export async function openhumanGetRuntimeFlags(): Promise> { if (!isTauri()) { throw new Error('Not running in Tauri'); diff --git a/src/core/logging.rs b/src/core/logging.rs index 90b30ab43..3466ac6c7 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -10,7 +10,9 @@ use nu_ansi_term::{Color, Style}; use tracing::{Event, Level}; use tracing_subscriber::fmt::format::{FormatEvent, FormatFields, Writer}; use tracing_subscriber::fmt::FmtContext; +use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::util::SubscriberInitExt; static INIT: Once = Once::new(); @@ -89,10 +91,25 @@ pub fn init_for_cli_run(verbose: bool) { let use_color = io::stderr().is_terminal(); - let _ = tracing_subscriber::fmt() + let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(use_color) - .with_env_filter(filter) - .event_format(CleanCliFormat) + .event_format(CleanCliFormat); + + let sentry_layer = + sentry::integrations::tracing::layer().event_filter(|md: &tracing::Metadata<'_>| { + match *md.level() { + Level::ERROR => sentry::integrations::tracing::EventFilter::Event, + Level::WARN | Level::INFO => { + sentry::integrations::tracing::EventFilter::Breadcrumb + } + _ => sentry::integrations::tracing::EventFilter::Ignore, + } + }); + + let _ = tracing_subscriber::registry() + .with(filter) + .with(fmt_layer) + .with(sentry_layer) .try_init(); let _ = tracing_log::LogTracer::init(); diff --git a/src/main.rs b/src/main.rs index 06ceda722..f164eb86c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,69 @@ +use once_cell::sync::Lazy; +use regex::Regex; + fn main() { + // Initialize Sentry as the very first operation so the guard outlives everything. + // If OPENHUMAN_SENTRY_DSN is unset or empty, sentry::init returns a no-op guard. + let _sentry_guard = sentry::init(sentry::ClientOptions { + dsn: std::env::var("OPENHUMAN_SENTRY_DSN") + .ok() + .filter(|s| !s.is_empty()) + .and_then(|s| s.parse().ok()), + release: Some(std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION"))), + environment: Some(if cfg!(debug_assertions) { + "development".into() + } else { + "production".into() + }), + send_default_pii: false, + before_send: Some(std::sync::Arc::new(|mut event| { + // Strip server_name (hostname) to avoid leaking machine identity + event.server_name = None; + // Strip user context entirely + event.user = None; + // Scrub exception messages for secrets + for exc in &mut event.exception.values { + if let Some(ref value) = exc.value { + exc.value = Some(scrub_secrets(value)); + } + } + Some(event) + })), + sample_rate: 1.0, + ..sentry::ClientOptions::default() + }); + let args: Vec = std::env::args().skip(1).collect(); if let Err(err) = openhuman_core::run_core_from_args(&args) { eprintln!("{err}"); std::process::exit(1); } } + +// --------------------------------------------------------------------------- +// Secret scrubbing +// --------------------------------------------------------------------------- + +static SECRET_PATTERNS: Lazy> = Lazy::new(|| { + vec![ + (Regex::new(r"(?i)(bearer\s+)\S+").unwrap(), "${1}[REDACTED]"), + ( + Regex::new(r"(?i)(api[_-]?key[=:\s]+)\S+").unwrap(), + "${1}[REDACTED]", + ), + ( + Regex::new(r"(?i)(token[=:\s]+)\S+").unwrap(), + "${1}[REDACTED]", + ), + (Regex::new(r"sk-[a-zA-Z0-9]{20,}").unwrap(), "[REDACTED]"), + ] +}); + +/// Replace patterns that look like secrets with `[REDACTED]`. +fn scrub_secrets(input: &str) -> String { + let mut result = input.to_string(); + for (re, replacement) in SECRET_PATTERNS.iter() { + result = re.replace_all(&result, *replacement).into_owned(); + } + result +} diff --git a/src/openhuman/config/ops.rs b/src/openhuman/config/ops.rs index b611c8a91..21dbacb58 100644 --- a/src/openhuman/config/ops.rs +++ b/src/openhuman/config/ops.rs @@ -88,6 +88,11 @@ pub struct ScreenIntelligenceSettingsPatch { pub denylist: Option>, } +#[derive(Debug, Clone, Default)] +pub struct AnalyticsSettingsPatch { + pub enabled: Option, +} + #[derive(Debug, Clone, Serialize)] pub struct RuntimeFlagsOut { pub browser_allow_all: bool, @@ -313,6 +318,31 @@ pub async fn load_and_apply_runtime_settings( apply_runtime_settings(&mut config, update).await } +pub async fn apply_analytics_settings( + config: &mut Config, + update: AnalyticsSettingsPatch, +) -> Result, String> { + if let Some(enabled) = update.enabled { + config.observability.analytics_enabled = enabled; + } + config.save().await.map_err(|e| e.to_string())?; + let snapshot = snapshot_config_json(config)?; + Ok(RpcOutcome::new( + snapshot, + vec![format!( + "analytics settings saved to {}", + config.config_path.display() + )], + )) +} + +pub async fn load_and_apply_analytics_settings( + update: AnalyticsSettingsPatch, +) -> Result, String> { + let mut config = load_config_with_timeout().await?; + apply_analytics_settings(&mut config, update).await +} + pub async fn load_and_apply_browser_settings( update: BrowserSettingsPatch, ) -> Result, String> { diff --git a/src/openhuman/config/schema/load.rs b/src/openhuman/config/schema/load.rs index 9af009ab9..cc31fd26f 100644 --- a/src/openhuman/config/schema/load.rs +++ b/src/openhuman/config/schema/load.rs @@ -592,6 +592,22 @@ impl Config { } } + if let Ok(dsn) = std::env::var("OPENHUMAN_SENTRY_DSN") { + let dsn = dsn.trim(); + if !dsn.is_empty() { + self.observability.sentry_dsn = Some(dsn.to_string()); + } + } + + if let Ok(flag) = std::env::var("OPENHUMAN_ANALYTICS_ENABLED") { + let normalized = flag.trim().to_ascii_lowercase(); + match normalized.as_str() { + "1" | "true" | "yes" | "on" => self.observability.analytics_enabled = true, + "0" | "false" | "no" | "off" => self.observability.analytics_enabled = false, + _ => {} + } + } + if self.proxy.enabled && self.proxy.scope == ProxyScope::Environment { self.proxy.apply_to_process_env(); } diff --git a/src/openhuman/config/schema/observability.rs b/src/openhuman/config/schema/observability.rs index 7f7a67aa3..0f3e1823a 100644 --- a/src/openhuman/config/schema/observability.rs +++ b/src/openhuman/config/schema/observability.rs @@ -15,6 +15,19 @@ pub struct ObservabilityConfig { /// Service name reported to the OTel collector. Defaults to "openhuman". #[serde(default)] pub otel_service_name: Option, + + /// Sentry DSN for error reporting. Overridden by `OPENHUMAN_SENTRY_DSN` env var. + #[serde(default)] + pub sentry_dsn: Option, + + /// Whether anonymized analytics and error reporting is enabled. + /// Defaults to `true`. Users can disable via settings or CLI. + #[serde(default = "default_analytics_enabled")] + pub analytics_enabled: bool, +} + +fn default_analytics_enabled() -> bool { + true } impl Default for ObservabilityConfig { @@ -23,6 +36,8 @@ impl Default for ObservabilityConfig { backend: "none".into(), otel_endpoint: None, otel_service_name: None, + sentry_dsn: None, + analytics_enabled: true, } } } diff --git a/src/openhuman/config/schemas.rs b/src/openhuman/config/schemas.rs index 691313db8..dc3de2b80 100644 --- a/src/openhuman/config/schemas.rs +++ b/src/openhuman/config/schemas.rs @@ -50,6 +50,11 @@ struct ScreenIntelligenceSettingsUpdate { denylist: Option>, } +#[derive(Debug, Deserialize)] +struct AnalyticsSettingsUpdate { + enabled: Option, +} + #[derive(Debug, Deserialize)] struct SetBrowserAllowAllParams { enabled: bool, @@ -80,6 +85,8 @@ pub fn all_controller_schemas() -> Vec { schemas("set_browser_allow_all"), schemas("workspace_onboarding_flag_exists"), schemas("workspace_onboarding_flag_set"), + schemas("update_analytics_settings"), + schemas("get_analytics_settings"), schemas("agent_server_status"), ] } @@ -134,6 +141,14 @@ pub fn all_registered_controllers() -> Vec { schema: schemas("workspace_onboarding_flag_set"), handler: handle_workspace_onboarding_flag_set, }, + RegisteredController { + schema: schemas("update_analytics_settings"), + handler: handle_update_analytics_settings, + }, + RegisteredController { + schema: schemas("get_analytics_settings"), + handler: handle_get_analytics_settings, + }, RegisteredController { schema: schemas("agent_server_status"), handler: handle_agent_server_status, @@ -363,6 +378,28 @@ pub fn schemas(function: &str) -> ControllerSchema { required: true, }], }, + "update_analytics_settings" => ControllerSchema { + namespace: "config", + function: "update_analytics_settings", + description: "Enable or disable anonymized analytics and error reporting.", + inputs: vec![optional_bool( + "enabled", + "Enable anonymized analytics and crash reports.", + )], + outputs: vec![json_output("snapshot", "Updated config snapshot.")], + }, + "get_analytics_settings" => ControllerSchema { + namespace: "config", + function: "get_analytics_settings", + description: "Read current analytics settings.", + inputs: vec![], + outputs: vec![FieldSchema { + name: "enabled", + ty: TypeSchema::Bool, + comment: "Whether anonymized analytics is enabled.", + required: true, + }], + }, "agent_server_status" => ControllerSchema { namespace: "config", function: "agent_server_status", @@ -503,6 +540,29 @@ fn handle_workspace_onboarding_flag_set(params: Map) -> Controlle }) } +fn handle_update_analytics_settings(params: Map) -> ControllerFuture { + Box::pin(async move { + let update = deserialize_params::(params)?; + let patch = config_rpc::AnalyticsSettingsPatch { + enabled: update.enabled, + }; + to_json(config_rpc::load_and_apply_analytics_settings(patch).await?) + }) +} + +fn handle_get_analytics_settings(_params: Map) -> ControllerFuture { + Box::pin(async { + let config = config_rpc::load_config_with_timeout().await?; + let result = serde_json::json!({ + "enabled": config.observability.analytics_enabled, + }); + to_json(RpcOutcome::new( + result, + vec!["analytics settings read".to_string()], + )) + }) +} + fn handle_agent_server_status(_params: Map) -> ControllerFuture { Box::pin(async { to_json(config_rpc::agent_server_status()) }) }