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:
@@ -294,6 +294,16 @@ fn is_updater_disabled() -> bool {
|
||||
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)]
|
||||
#[allow(dead_code)]
|
||||
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)
|
||||
})),
|
||||
..Default::default()
|
||||
@@ -374,6 +394,7 @@ pub fn run() {
|
||||
upload_file,
|
||||
get_environment_variable,
|
||||
get_executable_dir,
|
||||
set_webview_info,
|
||||
#[cfg(desktop)]
|
||||
is_updater_disabled,
|
||||
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")
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
@@ -106,7 +146,7 @@ pub extern "C" fn readest_sentry_dsn() -> *const std::os::raw::c_char {
|
||||
mod tests {
|
||||
use super::{
|
||||
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]
|
||||
@@ -192,4 +232,38 @@ mod tests {
|
||||
assert!(!is_ignored_browser_error("concurrent use forbidden"));
|
||||
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() {
|
||||
const execDir = await invoke<string>('get_executable_dir');
|
||||
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,
|
||||
// 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`
|
||||
|
||||
Reference in New Issue
Block a user