feat(sentry): add crash reporting for Android, iOS, desktop, and web (#4914)

Report crashes and unhandled errors from every layer to one Sentry project via a
single build-time SENTRY_DSN (empty => disabled, so local and fork builds do not
report):

- JS/WebView + Rust panics via tauri-plugin-sentry (rustls transport; minidump
  handler desktop-only).
- Android native (JVM/NDK/ANR) via sentry-android 8.47.0 with manifest auto-init
  (excludes the discontinued lifecycle-common-java8 transitive).
- iOS native via sentry-cocoa started from a tracked SentrySupport/+load bootstrap
  that reads the DSN from a Rust readest_sentry_dsn() FFI (no generated-file edits).
- SENTRY_DSN resolved at build time from the environment, then .env.local, then
  .env (build.rs bakes it via cargo:rustc-env; build.gradle.kts for Android). CI
  passes the SENTRY_DSN secret through the existing .env.local step.

Crashes + errors only: traces sample rate 0, no session replay, no PII.
Symbolication (source maps / ProGuard / dSYM upload) is a follow-up.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-04 00:26:26 +09:00
committed by GitHub
parent 6391bfe788
commit 967a7833ca
12 changed files with 701 additions and 7 deletions
+1
View File
@@ -142,6 +142,7 @@ jobs:
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env.local
cp .env.local apps/readest-app/.env.local
- name: install rclone
+1
View File
@@ -229,6 +229,7 @@ jobs:
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env.local
cp .env.local apps/readest-app/.env.local
- name: build and upload Android apks
Generated
+448 -5
View File
@@ -36,6 +36,7 @@ dependencies = [
"read-progress-stream",
"reqwest 0.12.28",
"semver",
"sentry",
"serde",
"serde_json",
"tauri",
@@ -57,6 +58,7 @@ dependencies = [
"tauri-plugin-os",
"tauri-plugin-persisted-scope",
"tauri-plugin-process",
"tauri-plugin-sentry",
"tauri-plugin-sharekit",
"tauri-plugin-shell",
"tauri-plugin-sign-in-with-apple",
@@ -75,6 +77,15 @@ dependencies = [
"zip 2.4.2",
]
[[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"
@@ -545,6 +556,21 @@ dependencies = [
"tracing",
]
[[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",
"rustc-demangle",
"windows-link 0.2.1",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -749,7 +775,7 @@ checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
dependencies = [
"borsh-derive",
"bytes",
"cfg_aliases",
"cfg_aliases 0.2.1",
]
[[package]]
@@ -1031,6 +1057,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
@@ -1341,6 +1373,30 @@ dependencies = [
"libc",
]
[[package]]
name = "crash-context"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3"
dependencies = [
"cfg-if",
"libc",
"mach2",
]
[[package]]
name = "crash-handler"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3"
dependencies = [
"cfg-if",
"crash-context",
"libc",
"mach2",
"parking_lot",
]
[[package]]
name = "crc32c"
version = "0.6.8"
@@ -1622,6 +1678,16 @@ dependencies = [
"keyring-core",
]
[[package]]
name = "debugid"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
dependencies = [
"serde",
"uuid 1.23.2",
]
[[package]]
name = "default-net"
version = "0.22.0"
@@ -2731,6 +2797,12 @@ dependencies = [
"weezl",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
[[package]]
name = "gio"
version = "0.18.4"
@@ -2895,6 +2967,17 @@ dependencies = [
"system-deps 7.0.8",
]
[[package]]
name = "goblin"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "gtk"
version = "0.18.2"
@@ -3069,6 +3152,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hostname"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
dependencies = [
"cfg-if",
"libc",
"windows-link 0.2.1",
]
[[package]]
name = "html5ever"
version = "0.29.1"
@@ -3984,6 +4078,15 @@ dependencies = [
"libc",
]
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -4127,6 +4230,75 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minidump-common"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77"
dependencies = [
"bitflags 2.11.1",
"debugid",
"num-derive",
"num-traits",
"range-map",
"scroll",
"smart-default",
]
[[package]]
name = "minidump-writer"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d"
dependencies = [
"bitflags 2.11.1",
"byteorder",
"cfg-if",
"crash-context",
"goblin",
"libc",
"log",
"mach2",
"memmap2",
"memoffset",
"minidump-common",
"nix 0.28.0",
"procfs-core",
"scroll",
"tempfile",
"thiserror 1.0.69",
]
[[package]]
name = "minidumper"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1"
dependencies = [
"cfg-if",
"crash-context",
"libc",
"log",
"minidump-writer",
"parking_lot",
"polling",
"scroll",
"thiserror 1.0.69",
"uds",
]
[[package]]
name = "minidumper-child"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4f23f835dbe67e44ddf884d3802ff549ca5948bf60e9fd70e9a13c96324d1"
dependencies = [
"crash-handler",
"minidumper",
"thiserror 1.0.69",
"uuid 1.23.2",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -4352,6 +4524,18 @@ dependencies = [
"libc",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.11.1",
"cfg-if",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.31.3"
@@ -4360,7 +4544,7 @@ checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags 2.11.1",
"cfg-if",
"cfg_aliases",
"cfg_aliases 0.2.1",
"libc",
]
@@ -4487,6 +4671,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "num-integer"
version = "0.1.46"
@@ -4857,6 +5052,15 @@ dependencies = [
"objc",
]
[[package]]
name = "object"
version = "0.37.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -5387,6 +5591,12 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "plain"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "plist"
version = "1.9.0"
@@ -5582,6 +5792,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "procfs-core"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29"
dependencies = [
"bitflags 2.11.1",
"hex",
]
[[package]]
name = "proptest"
version = "1.11.0"
@@ -5725,7 +5945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
dependencies = [
"bytes",
"cfg_aliases",
"cfg_aliases 0.2.1",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
@@ -5765,7 +5985,7 @@ version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
dependencies = [
"cfg_aliases",
"cfg_aliases 0.2.1",
"libc",
"once_cell",
"socket2",
@@ -5944,6 +6164,15 @@ dependencies = [
"rand_core 0.9.5",
]
[[package]]
name = "range-map"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f"
dependencies = [
"num-traits",
]
[[package]]
name = "rapidhash"
version = "4.4.1"
@@ -6079,6 +6308,7 @@ dependencies = [
"cookie",
"cookie_store",
"encoding_rs",
"futures-channel",
"futures-core",
"futures-util",
"h2",
@@ -6270,6 +6500,12 @@ dependencies = [
"wasm-bindgen",
]
[[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 = "1.1.0"
@@ -6333,6 +6569,7 @@ version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -6515,6 +6752,26 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scroll"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "sdd"
version = "3.0.10"
@@ -6597,6 +6854,113 @@ dependencies = [
"serde_core",
]
[[package]]
name = "sentry"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989425268ab5c011e06400187eed6c298272f8ef913e49fcadc3fda788b45030"
dependencies = [
"httpdate",
"reqwest 0.12.28",
"rustls",
"sentry-backtrace",
"sentry-contexts",
"sentry-core",
"sentry-panic",
"sentry-tracing",
"tokio",
"ureq",
]
[[package]]
name = "sentry-backtrace"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68e299dd3f7bcf676875eee852c9941e1d08278a743c32ca528e2debf846a653"
dependencies = [
"backtrace",
"regex",
"sentry-core",
]
[[package]]
name = "sentry-contexts"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac0c5d6892cd4c414492fc957477b620026fb3411fca9fa12774831da561c88"
dependencies = [
"hostname",
"libc",
"os_info",
"rustc_version",
"sentry-core",
"uname",
]
[[package]]
name = "sentry-core"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deaa38b94e70820ff3f1f9db3c8b0aef053b667be130f618e615e0ff2492cbcc"
dependencies = [
"rand 0.9.4",
"sentry-types",
"serde",
"serde_json",
"url",
]
[[package]]
name = "sentry-panic"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b7a23b13c004873de3ce7db86eb0f59fe4adfc655a31f7bbc17fd10bacc9bfe"
dependencies = [
"sentry-backtrace",
"sentry-core",
]
[[package]]
name = "sentry-rust-minidump"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63964525bf74b16233dbcfb307e11485ebd8ff8f87f6ae212b07ca7937cd2db1"
dependencies = [
"minidumper-child",
"sentry",
"thiserror 2.0.18",
]
[[package]]
name = "sentry-tracing"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac841c7050aa73fc2bec8f7d8e9cb1159af0b3095757b99820823f3e54e5080"
dependencies = [
"bitflags 2.11.1",
"sentry-backtrace",
"sentry-core",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "sentry-types"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e477f4d4db08ddb4ab553717a8d3a511bc9e81dde0c808c680feacbb8105c412"
dependencies = [
"debugid",
"hex",
"rand 0.9.4",
"serde",
"serde_json",
"thiserror 2.0.18",
"time",
"url",
"uuid 1.23.2",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -6989,6 +7353,17 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smart-default"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "socket2"
version = "0.6.4"
@@ -7994,6 +8369,22 @@ dependencies = [
"tauri-plugin",
]
[[package]]
name = "tauri-plugin-sentry"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7432b519b6d2d027082a940783c61ba9b684b38d8df7558cd5f924d87009b295"
dependencies = [
"base64 0.22.1",
"schemars 0.8.22",
"sentry",
"sentry-rust-minidump",
"serde",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]]
name = "tauri-plugin-sharekit"
version = "0.3.1"
@@ -8896,7 +9287,7 @@ dependencies = [
"branches",
"bumpalo",
"bytemuck",
"cfg_aliases",
"cfg_aliases 0.2.1",
"cfg_block",
"chrono",
"crc32c",
@@ -9102,6 +9493,15 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "uds"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661"
dependencies = [
"libc",
]
[[package]]
name = "uds_windows"
version = "1.2.1"
@@ -9113,6 +9513,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "uname"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
dependencies = [
"libc",
]
[[package]]
name = "unarray"
version = "0.1.4"
@@ -9219,6 +9628,34 @@ dependencies = [
"typenum",
]
[[package]]
name = "ureq"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
dependencies = [
"base64 0.22.1",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"ureq-proto",
"utf8-zero",
"webpki-roots 1.0.7",
]
[[package]]
name = "ureq-proto"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
dependencies = [
"base64 0.22.1",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -9262,6 +9699,12 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091"
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
+14
View File
@@ -99,6 +99,20 @@ image = { version = "0.25", default-features = false, features = ["jpeg", "png",
# WebView). Pure-Rust crate, ships to every Tauri target.
mobi = "0.8"
# Crash/error reporting. `tauri-plugin-sentry` injects @sentry/browser into
# every webview and routes browser + Rust panic events through one client; it
# re-exports sentry-rust-minidump for desktop native crashes. `rustls` avoids
# the native-tls/OpenSSL system dependency so the transport builds for
# Android/iOS. Native mobile crashes are handled by sentry-android/-cocoa.
sentry = { version = "0.42", default-features = false, features = [
"backtrace",
"contexts",
"panic",
"reqwest",
"rustls",
] }
tauri-plugin-sentry = "0.5"
[target."cfg(target_os = \"macos\")".dependencies]
rand = "0.8"
cocoa = "0.25"
+56 -1
View File
@@ -1,4 +1,8 @@
use std::{env, fs, path::PathBuf, process::Command};
use std::{
env, fs,
path::{Path, PathBuf},
process::Command,
};
fn main() {
println!("cargo:rerun-if-changed=../extensions/windows-thumbnail/src");
@@ -7,9 +11,60 @@ fn main() {
build_windows_thumbnail();
}
propagate_sentry_dsn();
tauri_build::build()
}
/// Bake the Sentry DSN into the crate at build time via `cargo:rustc-env`, so
/// `option_env!("SENTRY_DSN")` (and, on iOS, the `readest_sentry_dsn` FFI) sees
/// it. Precedence: an existing `SENTRY_DSN` in the environment (CI secret / shell
/// export) wins; otherwise fall back to the gitignored `.env.local`, then `.env`,
/// at the app root. Absent everywhere => unset, so reporting stays disabled for
/// local and fork builds. `rerun-if-*` makes cargo recompile when the value or
/// the dotenv files change (avoiding a stale baked-in value).
fn propagate_sentry_dsn() {
println!("cargo:rerun-if-env-changed=SENTRY_DSN");
let app_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("..");
let env_local = app_dir.join(".env.local");
let env_file = app_dir.join(".env");
println!("cargo:rerun-if-changed={}", env_local.display());
println!("cargo:rerun-if-changed={}", env_file.display());
let dsn = env::var("SENTRY_DSN")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| read_env_value(&env_local, "SENTRY_DSN"))
.or_else(|| read_env_value(&env_file, "SENTRY_DSN"));
if let Some(dsn) = dsn {
println!("cargo:rustc-env=SENTRY_DSN={dsn}");
}
}
/// Read a single `KEY=value` from a dotenv-style file, skipping blank lines and
/// `#` comments and stripping surrounding quotes. `None` if the file/key is
/// absent or the value is empty.
fn read_env_value(path: &Path, key: &str) -> Option<String> {
let contents = fs::read_to_string(path).ok()?;
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(value) = line
.strip_prefix(key)
.and_then(|rest| rest.trim_start().strip_prefix('='))
{
let value = value.trim().trim_matches(|c| c == '"' || c == '\'');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
fn build_windows_thumbnail() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let dll_crate_dir = manifest_dir
@@ -188,6 +188,7 @@
"device-info:default",
"turso:default",
"native-tts:default",
"native-bridge:default"
"native-bridge:default",
"sentry:default"
]
}
@@ -24,6 +24,20 @@ android {
}
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
// Sentry DSN precedence: environment (CI secret / shell export) wins,
// else the gitignored .env.local, else .env at the app root (../../../
// from this module). Empty => Sentry auto-init no-ops.
manifestPlaceholders["sentryDsn"] = System.getenv("SENTRY_DSN")?.takeIf { it.isNotBlank() }
?: listOf("../../../.env.local", "../../../.env")
.map { rootProject.file(it) }
.filter { it.exists() }
.firstNotNullOfOrNull { f ->
f.readLines()
.map { it.trim() }
.firstOrNull { it.startsWith("SENTRY_DSN=") }
?.substringAfter("=")?.trim()?.trim('"', '\'')?.takeIf { it.isNotEmpty() }
}
?: ""
applicationId = "com.bilingify.readest"
minSdk = 26
targetSdk = 36
@@ -88,6 +102,15 @@ dependencies {
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
implementation("io.sentry:sentry-android:8.47.0") {
// androidx.lifecycle:lifecycle-common-java8 was merged into
// lifecycle-common and is no longer published at 2.9.0+. The project
// pins androidx.lifecycle to 2.10.0 (lifecycle-process above), which
// version-aligns this Sentry transitive to a nonexistent 2.10.0 and
// breaks dependency resolution. The Java8 lifecycle APIs Sentry uses now
// live in lifecycle-common, pulled in transitively via lifecycle-process.
exclude(group = "androidx.lifecycle", module = "lifecycle-common-java8")
}
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
@@ -29,6 +29,12 @@
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<!-- Sentry auto-init (ContentProvider). Empty DSN => disabled. Release
is auto-detected from versionName; performance and PII stay at
their safe off/false defaults. -->
<meta-data android:name="io.sentry.dsn" android:value="${sentryDsn}" />
<meta-data android:name="io.sentry.environment" android:value="production" />
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
@@ -0,0 +1,31 @@
#import <Foundation/Foundation.h>
@import Sentry;
// Provided by the Rust static library (see src-tauri/src/sentry_config.rs).
extern const char *readest_sentry_dsn(void);
// Starts sentry-cocoa at class-load time (before main), so native iOS crashes
// are captured from launch without editing the generated main.mm / Info.plist.
// The DSN comes from the Rust side (single SENTRY_DSN build-time source); a null
// or empty DSN leaves the native SDK disabled, so local and fork builds do not
// report. Crashes + errors only: no tracing, no PII.
@interface ReadestSentryBootstrap : NSObject
@end
@implementation ReadestSentryBootstrap
+ (void)load {
const char *dsn = readest_sentry_dsn();
if (dsn == NULL || dsn[0] == '\0') {
return;
}
NSString *dsnString = [NSString stringWithUTF8String:dsn];
[SentrySDK startWithConfigureOptions:^(SentryOptions *options) {
options.dsn = dsnString;
options.environment = @"production";
options.tracesSampleRate = @0.0;
options.sendDefaultPii = NO;
}];
}
@end
@@ -4,6 +4,10 @@ options:
deploymentTarget:
iOS: 15.0
fileGroups: [../../src]
packages:
Sentry:
url: https://github.com/getsentry/sentry-cocoa
from: 8.0.0
configs:
debug: debug
release: release
@@ -45,6 +49,7 @@ targets:
# than ship a malformed one. IAP works without it.
sources:
- path: Sources
- path: SentrySupport
- path: Assets.xcassets
# Only reference Externals as a file group (no compile/resource
# phases) — the actual libapp.a lookup happens via the
@@ -92,6 +97,8 @@ targets:
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64
groups: [app]
dependencies:
- package: Sentry
product: Sentry
- framework: libapp.a
embed: false
- target: ShareExtension
+30
View File
@@ -33,6 +33,7 @@ mod mobi_parser;
mod nightly_update;
mod parser_common;
mod range_file;
mod sentry_config;
#[cfg(desktop)]
mod spawn_fresh_browser;
mod transfer_file;
@@ -302,6 +303,30 @@ struct SingleInstancePayload {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// Initialize Sentry as early as possible so panics during startup are
// captured. `None` DSN (unset SENTRY_DSN) => disabled, so local and fork
// builds don't report. Desktop also starts the out-of-process minidump
// handler for native crashes; on mobile, native crashes belong to the
// sentry-android / sentry-cocoa SDKs. The guard must outlive the app, so it
// is held until `run()` returns (after the blocking `.run(...)` call).
let sentry_guard = sentry_config::sentry_dsn().map(|dsn| {
sentry::init((
dsn,
sentry::ClientOptions {
release: sentry::release_name!(),
environment: Some(sentry_config::sentry_environment().into()),
traces_sample_rate: 0.0,
send_default_pii: false,
..Default::default()
},
))
});
#[cfg(not(any(target_os = "ios", target_os = "android")))]
let _minidump_guard = sentry_guard
.as_ref()
.map(|guard| tauri_plugin_sentry::minidump::init(guard));
let builder = tauri::Builder::default()
.plugin(
tauri_plugin_log::Builder::new()
@@ -417,6 +442,11 @@ pub fn run() {
#[cfg(feature = "webdriver")]
let builder = builder.plugin(tauri_plugin_webdriver::init());
let builder = match sentry_guard.as_ref() {
Some(client) => builder.plugin(tauri_plugin_sentry::init(client)),
None => builder,
};
builder
.setup(|#[allow(unused_variables)] app| {
// When running with the webdriver feature (E2E/integration tests),
@@ -0,0 +1,82 @@
//! Pure helpers for configuring Sentry. Kept dependency-free and unit-tested;
//! the actual `sentry::init` call and plugin registration live in `lib.rs`.
/// Normalizes a raw `SENTRY_DSN` value. Unset, empty, or whitespace-only maps to
/// `None` so local and fork builds (no DSN) never report.
pub fn dsn_from_env(raw: Option<&str>) -> Option<&str> {
match raw {
Some(dsn) if !dsn.trim().is_empty() => Some(dsn.trim()),
_ => None,
}
}
/// Compile-time Sentry DSN from the `SENTRY_DSN` env var (baked at build time).
pub fn sentry_dsn() -> Option<&'static str> {
dsn_from_env(option_env!("SENTRY_DSN"))
}
/// Sentry `environment` tag derived from the crate version. Nightly builds use a
/// single 10-digit `YYYYMMDDHH` prerelease (e.g. `0.11.17-2026070301`);
/// everything else is treated as production.
pub fn environment_for_version(version: &str) -> &'static str {
if let Some((_, pre)) = version.split_once('-') {
if pre.len() == 10 && pre.bytes().all(|b| b.is_ascii_digit()) {
return "nightly";
}
}
"production"
}
/// Sentry `environment` for the current build.
pub fn sentry_environment() -> &'static str {
environment_for_version(env!("CARGO_PKG_VERSION"))
}
/// C-ABI accessor for the compile-time Sentry DSN, used by the iOS native
/// bootstrap (sentry-cocoa) so it starts with the same DSN as the Rust client
/// without a second env read or fragile Info.plist / preprocessor plumbing.
/// Returns a NUL-terminated pointer valid for the process lifetime, or null when
/// no DSN is configured (empty `SENTRY_DSN` => the native SDK stays disabled).
#[cfg(target_os = "ios")]
#[no_mangle]
pub extern "C" fn readest_sentry_dsn() -> *const std::os::raw::c_char {
use std::ffi::CString;
use std::sync::OnceLock;
static DSN: OnceLock<Option<CString>> = OnceLock::new();
match DSN.get_or_init(|| sentry_dsn().and_then(|d| CString::new(d).ok())) {
Some(cstr) => cstr.as_ptr(),
None => std::ptr::null(),
}
}
#[cfg(test)]
mod tests {
use super::{dsn_from_env, environment_for_version};
#[test]
fn dsn_is_none_when_unset_or_blank() {
assert_eq!(dsn_from_env(None), None);
assert_eq!(dsn_from_env(Some("")), None);
assert_eq!(dsn_from_env(Some(" ")), None);
}
#[test]
fn dsn_is_trimmed_when_present() {
assert_eq!(
dsn_from_env(Some(" https://k@o.ingest.sentry.io/1 ")),
Some("https://k@o.ingest.sentry.io/1")
);
}
#[test]
fn environment_is_nightly_for_ten_digit_prerelease() {
assert_eq!(environment_for_version("0.11.17-2026070301"), "nightly");
}
#[test]
fn environment_is_production_otherwise() {
assert_eq!(environment_for_version("0.11.17"), "production");
assert_eq!(environment_for_version("0.11.17-rc.1"), "production");
assert_eq!(environment_for_version("1.0.0-2026"), "production");
}
}