feat(sentry): tag events with the WebView engine and version (#4952)

Forwarded browser events carry os/rust/device context but no browser context,
so crashes couldn't be correlated with the WebView version. The app now reports
its User-Agent at startup via a set_webview_info command; the parsed engine
(Chromium/WebKit) and major version are stored and attached as webview.engine
and webview.version tags in before_send, covering both Rust panics and
forwarded browser events.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-06 04:12:21 +09:00
committed by GitHub
parent 52be6fa066
commit da00a94f66
3 changed files with 103 additions and 1 deletions
+21
View File
@@ -294,6 +294,16 @@ fn is_updater_disabled() -> bool {
updater_disabled() updater_disabled()
} }
// Record the WebView engine/version (parsed from the app's User-Agent) so Sentry
// events can be correlated with WebView version. Called once from
// `NativeAppService.init()`; no-op when Sentry is disabled.
#[tauri::command]
fn set_webview_info(user_agent: String) {
if let Some((engine, version)) = sentry_config::parse_webview_info(&user_agent) {
sentry_config::set_webview_info(engine, version);
}
}
#[derive(Clone, serde::Serialize)] #[derive(Clone, serde::Serialize)]
#[allow(dead_code)] #[allow(dead_code)]
struct SingleInstancePayload { struct SingleInstancePayload {
@@ -345,6 +355,16 @@ pub fn run() {
} }
} }
} }
// Tag the WebView engine/version (reported by the app at
// startup) so crashes can be correlated with it.
if let Some((engine, version)) = sentry_config::webview_info() {
event
.tags
.insert("webview.engine".to_string(), engine.clone());
event
.tags
.insert("webview.version".to_string(), version.clone());
}
Some(event) Some(event)
})), })),
..Default::default() ..Default::default()
@@ -374,6 +394,7 @@ pub fn run() {
upload_file, upload_file,
get_environment_variable, get_environment_variable,
get_executable_dir, get_executable_dir,
set_webview_info,
#[cfg(desktop)] #[cfg(desktop)]
is_updater_disabled, is_updater_disabled,
allow_paths_in_scopes, allow_paths_in_scopes,
@@ -85,6 +85,46 @@ pub fn is_ignored_browser_error(value: &str) -> bool {
value.contains("View transition was skipped because document visibility state is hidden") value.contains("View transition was skipped because document visibility state is hidden")
} }
/// The WebView (engine, major-version), set once at startup when the app reports
/// its User-Agent. Stored globally so `before_send` can tag every event — the
/// browser context integration doesn't run for events forwarded from the webview.
static WEBVIEW_INFO: std::sync::OnceLock<(String, String)> = std::sync::OnceLock::new();
/// Record the WebView engine + version. No-op if already set.
pub fn set_webview_info(engine: String, version: String) {
let _ = WEBVIEW_INFO.set((engine, version));
}
/// The recorded WebView `(engine, version)`, if the app has reported it yet.
pub fn webview_info() -> Option<&'static (String, String)> {
WEBVIEW_INFO.get()
}
/// Parse the WebView engine and major version from a User-Agent string. Chromium
/// WebViews (Android System WebView, Windows WebView2, Linux Chrome) carry a
/// `Chrome/<v>` token; WebKit ones (iOS/macOS WKWebView, Linux WebKitGTK) carry
/// `Version/<v>` and no `Chrome/`. Chrome is checked first because Android
/// WebViews also include a legacy `Version/4.0`. `None` if neither is present.
pub fn parse_webview_info(user_agent: &str) -> Option<(String, String)> {
if let Some(v) = ua_major_version(user_agent, "Chrome/") {
return Some(("Chromium".to_string(), v));
}
if let Some(v) = ua_major_version(user_agent, "Version/") {
return Some(("WebKit".to_string(), v));
}
None
}
fn ua_major_version(user_agent: &str, token: &str) -> Option<String> {
let rest = &user_agent[user_agent.find(token)? + token.len()..];
let major: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
if major.is_empty() {
None
} else {
Some(major)
}
}
/// C-ABI accessor for the compile-time Sentry DSN, used by the iOS native /// 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 /// 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. /// without a second env read or fragile Info.plist / preprocessor plumbing.
@@ -106,7 +146,7 @@ pub extern "C" fn readest_sentry_dsn() -> *const std::os::raw::c_char {
mod tests { mod tests {
use super::{ use super::{
android_version_from_uname, corrected_os_name, dsn_from_env, environment_for_version, android_version_from_uname, corrected_os_name, dsn_from_env, environment_for_version,
is_ignored_browser_error, release_name, is_ignored_browser_error, parse_webview_info, release_name,
}; };
#[test] #[test]
@@ -192,4 +232,38 @@ mod tests {
assert!(!is_ignored_browser_error("concurrent use forbidden")); assert!(!is_ignored_browser_error("concurrent use forbidden"));
assert!(!is_ignored_browser_error("")); assert!(!is_ignored_browser_error(""));
} }
#[test]
fn parses_chromium_webview_version() {
// Android System WebView carries a legacy `Version/4.0` AND `Chrome/140`;
// Chrome must win.
let ua = "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) \
Version/4.0 Chrome/140.0.0.0 Mobile Safari/537.36";
assert_eq!(
parse_webview_info(ua),
Some(("Chromium".to_string(), "140".to_string()))
);
}
#[test]
fn parses_webkit_webview_version() {
let ios = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) \
AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Mobile/15E148 Safari/604.1";
assert_eq!(
parse_webview_info(ios),
Some(("WebKit".to_string(), "17".to_string()))
);
let gtk = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) \
Version/2.44.0 Safari/605.1.15";
assert_eq!(
parse_webview_info(gtk),
Some(("WebKit".to_string(), "2".to_string()))
);
}
#[test]
fn webview_info_is_none_for_unrecognized_ua() {
assert_eq!(parse_webview_info("curl/8.0"), None);
assert_eq!(parse_webview_info(""), None);
}
} }
@@ -595,6 +595,13 @@ export class NativeAppService extends BaseAppService {
override async init() { override async init() {
const execDir = await invoke<string>('get_executable_dir'); const execDir = await invoke<string>('get_executable_dir');
this.execDir = execDir; this.execDir = execDir;
// Report the WebView User-Agent so Sentry can tag crashes with the
// engine/version (the injected browser SDK's UA context isn't forwarded).
try {
await invoke('set_webview_info', { userAgent: navigator.userAgent });
} catch (err) {
console.warn('[nativeAppService] set_webview_info failed:', err);
}
// Ask Rust whether the in-app updater must stay hidden (READEST_DISABLE_UPDATER, // Ask Rust whether the in-app updater must stay hidden (READEST_DISABLE_UPDATER,
// Flatpak, or a Linux deb/rpm/pacman install that Tauri can't self-update). The // Flatpak, or a Linux deb/rpm/pacman install that Tauri can't self-update). The
// command is the reliable source of truth; the `__READEST_UPDATER_DISABLED` // command is the reliable source of truth; the `__READEST_UPDATER_DISABLED`