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()
|
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`
|
||||||
|
|||||||
Reference in New Issue
Block a user