Skip to main content

tauri_plugin_sentry/
lib.rs

1use std::{borrow::Cow, sync::Arc};
2
3use sentry::{
4    protocol::{ClientSdkInfo, ClientSdkPackage, Event},
5    ClientOptions, Hub,
6};
7use tauri::{
8    generate_handler,
9    ipc::Invoke,
10    plugin::{Builder, Plugin, TauriPlugin},
11    webview::PageLoadPayload,
12    AppHandle, Manager, RunEvent, Runtime, Url, Webview, Window,
13};
14
15#[derive(Debug, Clone)]
16pub struct JavaScriptOptions {
17    pub inject: bool,
18    pub debug: bool,
19}
20
21impl JavaScriptOptions {
22    pub fn no_injection() -> Self {
23        Self {
24            inject: false,
25            ..Default::default()
26        }
27    }
28}
29
30impl Default for JavaScriptOptions {
31    fn default() -> Self {
32        Self {
33            inject: true,
34            #[cfg(not(debug_assertions))]
35            debug: false,
36            #[cfg(debug_assertions)]
37            debug: true,
38        }
39    }
40}
41
42#[derive(Debug, Clone, Default)]
43pub struct Options {
44    pub javascript: JavaScriptOptions,
45    pub breadcrumbs: BreadcrumbOptions,
46}
47
48mod breadcrumbs;
49mod commands;
50mod context;
51mod process;
52mod scope;
53#[cfg(feature = "webview-crashes")]
54mod webview_crash;
55
56pub use breadcrumbs::{BreadcrumbOptions, EventFilter};
57#[cfg(feature = "webview-crashes")]
58pub use webview_crash::web_content_process_terminated;
59
60fn js_init_script(options: &JavaScriptOptions) -> String {
61    include_str!("../dist/inject.min.js").replace("__DEBUG__", &format!("{}", options.debug))
62}
63
64pub(crate) fn add_sdk_package(sdk: &mut Cow<'_, ClientSdkInfo>) {
65    sdk.to_mut().packages.push(ClientSdkPackage {
66        name: "cargo:tauri-plugin-sentry".into(),
67        version: env!("CARGO_PKG_VERSION").into(),
68    });
69}
70
71/// A Tauri plugin that is also a Sentry integration.
72///
73/// Add a clone to the Sentry `ClientOptions` before `sentry::init` and
74/// pass the original to `tauri::Builder::plugin`.
75#[derive(Debug, Clone, Default)]
76pub struct Sentry {
77    options: Arc<Options>,
78    context: Arc<context::TauriContext>,
79}
80
81impl Sentry {
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    pub fn with_options(options: Options) -> Self {
87        Self {
88            options: Arc::new(options),
89            ..Default::default()
90        }
91    }
92}
93
94impl sentry::Integration for Sentry {
95    fn name(&self) -> &'static str {
96        "tauri"
97    }
98
99    fn process_event(
100        &self,
101        mut event: Event<'static>,
102        _options: &ClientOptions,
103    ) -> Option<Event<'static>> {
104        if let Some(sdk) = event.sdk.as_mut() {
105            add_sdk_package(sdk);
106        }
107        process::tag_rust_event(&mut event.tags);
108        self.context.apply(&mut event);
109        Some(event)
110    }
111}
112
113impl<R: Runtime> Plugin<R> for Sentry {
114    fn name(&self) -> &'static str {
115        "sentry"
116    }
117
118    fn initialize(
119        &mut self,
120        app: &AppHandle<R>,
121        _config: serde_json::Value,
122    ) -> Result<(), Box<dyn std::error::Error>> {
123        let client = Hub::main()
124            .client()
125            .ok_or("sentry::init must be called before the Sentry plugin is initialized")?;
126        app.manage((*client).clone());
127        self.context.initialize(app);
128        Ok(())
129    }
130
131    fn initialization_script(&self) -> Option<String> {
132        self.options
133            .javascript
134            .inject
135            .then(|| js_init_script(&self.options.javascript))
136    }
137
138    fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
139        let handler: fn(Invoke<R>) -> bool =
140            generate_handler![commands::breadcrumb, commands::envelope, scope::scope];
141        handler(invoke)
142    }
143
144    fn window_created(&mut self, window: Window<R>) {
145        breadcrumbs::on_window_created(&self.options.breadcrumbs, &window);
146    }
147
148    fn webview_created(&mut self, webview: Webview<R>) {
149        breadcrumbs::on_webview_created(&self.options.breadcrumbs, &webview);
150        #[cfg(feature = "webview-crashes")]
151        webview_crash::install(&webview);
152    }
153
154    fn on_navigation(&mut self, webview: &Webview<R>, url: &Url) -> bool {
155        breadcrumbs::on_navigation(&self.options.breadcrumbs, webview, url);
156        true
157    }
158
159    fn on_page_load(&mut self, webview: &Webview<R>, payload: &PageLoadPayload<'_>) {
160        breadcrumbs::on_page_load(&self.options.breadcrumbs, webview, payload);
161    }
162
163    fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
164        breadcrumbs::on_event(&self.options.breadcrumbs, app, event);
165        self.context.on_event(app, event);
166    }
167}
168
169pub fn init_with_options<R: Runtime>(
170    sentry_client: &sentry::Client,
171    options: Options,
172) -> TauriPlugin<R> {
173    let sentry_client = sentry_client.clone();
174
175    let mut plugin_builder = Builder::<R>::new("sentry")
176        .invoke_handler(generate_handler![
177            commands::breadcrumb,
178            commands::envelope,
179            scope::scope
180        ])
181        .setup(move |app, _| {
182            app.manage(sentry_client);
183            Ok(())
184        });
185
186    if options.javascript.inject {
187        plugin_builder = plugin_builder.js_init_script(js_init_script(&options.javascript));
188    }
189
190    breadcrumbs::attach(plugin_builder, Arc::new(options.breadcrumbs)).build()
191}
192
193pub fn init<R: Runtime>(sentry_client: &sentry::Client) -> TauriPlugin<R> {
194    init_with_options(sentry_client, Default::default())
195}
196
197pub fn init_with_no_injection<R: Runtime>(sentry_client: &sentry::Client) -> TauriPlugin<R> {
198    init_with_options(
199        sentry_client,
200        Options {
201            javascript: JavaScriptOptions::no_injection(),
202            ..Default::default()
203        },
204    )
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn integration_adds_sdk_metadata() {
213        let options = ClientOptions::new().add_integration(Sentry::new());
214        let events = sentry::test::with_captured_events_options(
215            || {
216                sentry::capture_message("test", sentry::Level::Info);
217            },
218            options,
219        );
220
221        let sdk = events[0].sdk.as_ref().unwrap();
222        assert!(sdk.integrations.iter().any(|i| i == "tauri"));
223        assert!(sdk
224            .packages
225            .iter()
226            .any(|p| p.name == "cargo:tauri-plugin-sentry"));
227        assert_eq!(events[0].tags["event.process"], "rust");
228    }
229}