use std::{borrow::Cow, sync::Arc};
use sentry::{
protocol::{ClientSdkInfo, ClientSdkPackage, Event},
ClientOptions, Hub,
};
use tauri::{
generate_handler,
ipc::Invoke,
plugin::{Builder, Plugin, TauriPlugin},
webview::PageLoadPayload,
AppHandle, Manager, RunEvent, Runtime, Url, Webview, Window,
};
#[derive(Debug, Clone)]
pub struct JavaScriptOptions {
pub inject: bool,
pub debug: bool,
}
impl JavaScriptOptions {
pub fn no_injection() -> Self {
Self {
inject: false,
..Default::default()
}
}
}
impl Default for JavaScriptOptions {
fn default() -> Self {
Self {
inject: true,
#[cfg(not(debug_assertions))]
debug: false,
#[cfg(debug_assertions)]
debug: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Options {
pub javascript: JavaScriptOptions,
pub breadcrumbs: BreadcrumbOptions,
}
mod breadcrumbs;
mod commands;
mod context;
mod process;
mod scope;
#[cfg(feature = "webview-crashes")]
mod webview_crash;
pub use breadcrumbs::{BreadcrumbOptions, EventFilter};
#[cfg(feature = "webview-crashes")]
pub use webview_crash::web_content_process_terminated;
fn js_init_script(options: &JavaScriptOptions) -> String {
include_str!("../dist/inject.min.js").replace("__DEBUG__", &format!("{}", options.debug))
}
pub(crate) fn add_sdk_package(sdk: &mut Cow<'_, ClientSdkInfo>) {
sdk.to_mut().packages.push(ClientSdkPackage {
name: "cargo:tauri-plugin-sentry".into(),
version: env!("CARGO_PKG_VERSION").into(),
});
}
#[derive(Debug, Clone, Default)]
pub struct Sentry {
options: Arc<Options>,
context: Arc<context::TauriContext>,
}
impl Sentry {
pub fn new() -> Self {
Self::default()
}
pub fn with_options(options: Options) -> Self {
Self {
options: Arc::new(options),
..Default::default()
}
}
}
impl sentry::Integration for Sentry {
fn name(&self) -> &'static str {
"tauri"
}
fn process_event(
&self,
mut event: Event<'static>,
_options: &ClientOptions,
) -> Option<Event<'static>> {
if let Some(sdk) = event.sdk.as_mut() {
add_sdk_package(sdk);
}
process::tag_rust_event(&mut event.tags);
self.context.apply(&mut event);
Some(event)
}
}
impl<R: Runtime> Plugin<R> for Sentry {
fn name(&self) -> &'static str {
"sentry"
}
fn initialize(
&mut self,
app: &AppHandle<R>,
_config: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Hub::main()
.client()
.ok_or("sentry::init must be called before the Sentry plugin is initialized")?;
app.manage((*client).clone());
self.context.initialize(app);
Ok(())
}
fn initialization_script(&self) -> Option<String> {
self.options
.javascript
.inject
.then(|| js_init_script(&self.options.javascript))
}
fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
let handler: fn(Invoke<R>) -> bool =
generate_handler![commands::breadcrumb, commands::envelope, scope::scope];
handler(invoke)
}
fn window_created(&mut self, window: Window<R>) {
breadcrumbs::on_window_created(&self.options.breadcrumbs, &window);
}
fn webview_created(&mut self, webview: Webview<R>) {
breadcrumbs::on_webview_created(&self.options.breadcrumbs, &webview);
#[cfg(feature = "webview-crashes")]
webview_crash::install(&webview);
}
fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
breadcrumbs::on_navigation(&self.options.breadcrumbs, webview, url);
true
}
fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
breadcrumbs::on_page_load(&self.options.breadcrumbs, webview, payload);
}
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
breadcrumbs::on_event(&self.options.breadcrumbs, app, event);
self.context.on_event(app, event);
}
}
pub fn init_with_options<R: Runtime>(
sentry_client: &sentry::Client,
options: Options,
) -> TauriPlugin<R> {
let sentry_client = sentry_client.clone();
let mut plugin_builder = Builder::<R>::new("sentry")
.invoke_handler(generate_handler![
commands::breadcrumb,
commands::envelope,
scope::scope
])
.setup(move |app, _| {
app.manage(sentry_client);
Ok(())
});
if options.javascript.inject {
plugin_builder = plugin_builder.js_init_script(js_init_script(&options.javascript));
}
breadcrumbs::attach(plugin_builder, Arc::new(options.breadcrumbs)).build()
}
pub fn init<R: Runtime>(sentry_client: &sentry::Client) -> TauriPlugin<R> {
init_with_options(sentry_client, Default::default())
}
pub fn init_with_no_injection<R: Runtime>(sentry_client: &sentry::Client) -> TauriPlugin<R> {
init_with_options(
sentry_client,
Options {
javascript: JavaScriptOptions::no_injection(),
..Default::default()
},
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integration_adds_sdk_metadata() {
let options = ClientOptions::new().add_integration(Sentry::new());
let events = sentry::test::with_captured_events_options(
|| {
sentry::capture_message("test", sentry::Level::Info);
},
options,
);
let sdk = events[0].sdk.as_ref().unwrap();
assert!(sdk.integrations.iter().any(|i| i == "tauri"));
assert!(sdk
.packages
.iter()
.any(|p| p.name == "cargo:tauri-plugin-sentry"));
assert_eq!(events[0].tags["event.process"], "rust");
}
}