tauri_plugin_sentry/
lib.rs1use 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#[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}