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 AppHandle, Manager, Runtime,
12};
13
14#[derive(Debug, Clone)]
15pub struct JavaScriptOptions {
16 pub inject: bool,
17 pub debug: bool,
18}
19
20impl JavaScriptOptions {
21 pub fn no_injection() -> Self {
22 Self {
23 inject: false,
24 ..Default::default()
25 }
26 }
27}
28
29impl Default for JavaScriptOptions {
30 fn default() -> Self {
31 Self {
32 inject: true,
33 #[cfg(not(debug_assertions))]
34 debug: false,
35 #[cfg(debug_assertions)]
36 debug: true,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Default)]
42pub struct Options {
43 pub javascript: JavaScriptOptions,
44}
45
46mod commands;
47
48fn js_init_script(options: &JavaScriptOptions) -> String {
49 include_str!("../dist/inject.min.js").replace("__DEBUG__", &format!("{}", options.debug))
50}
51
52pub(crate) fn add_sdk_package(sdk: &mut Cow<'_, ClientSdkInfo>) {
53 sdk.to_mut().packages.push(ClientSdkPackage {
54 name: "cargo:tauri-plugin-sentry".into(),
55 version: env!("CARGO_PKG_VERSION").into(),
56 });
57}
58
59#[derive(Debug, Clone, Default)]
64pub struct Sentry {
65 options: Arc<Options>,
66}
67
68impl Sentry {
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn with_options(options: Options) -> Self {
74 Self {
75 options: Arc::new(options),
76 }
77 }
78}
79
80impl sentry::Integration for Sentry {
81 fn name(&self) -> &'static str {
82 "tauri"
83 }
84
85 fn process_event(
86 &self,
87 mut event: Event<'static>,
88 _options: &ClientOptions,
89 ) -> Option<Event<'static>> {
90 if let Some(sdk) = event.sdk.as_mut() {
91 add_sdk_package(sdk);
92 }
93 Some(event)
94 }
95}
96
97impl<R: Runtime> Plugin<R> for Sentry {
98 fn name(&self) -> &'static str {
99 "sentry"
100 }
101
102 fn initialize(
103 &mut self,
104 app: &AppHandle<R>,
105 _config: serde_json::Value,
106 ) -> Result<(), Box<dyn std::error::Error>> {
107 let client = Hub::main()
108 .client()
109 .ok_or("sentry::init must be called before the Sentry plugin is initialized")?;
110 app.manage((*client).clone());
111 Ok(())
112 }
113
114 fn initialization_script(&self) -> Option<String> {
115 self.options
116 .javascript
117 .inject
118 .then(|| js_init_script(&self.options.javascript))
119 }
120
121 fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
122 let handler: fn(Invoke<R>) -> bool =
123 generate_handler![commands::breadcrumb, commands::envelope];
124 handler(invoke)
125 }
126}
127
128pub fn init_with_options<R: Runtime>(
129 sentry_client: &sentry::Client,
130 options: Options,
131) -> TauriPlugin<R> {
132 let sentry_client = sentry_client.clone();
133
134 let mut plugin_builder = Builder::<R>::new("sentry")
135 .invoke_handler(generate_handler![commands::breadcrumb, commands::envelope])
136 .setup(move |app, _| {
137 app.manage(sentry_client);
138 Ok(())
139 });
140
141 if options.javascript.inject {
142 plugin_builder = plugin_builder.js_init_script(js_init_script(&options.javascript));
143 }
144
145 plugin_builder.build()
146}
147
148pub fn init<R: Runtime>(sentry_client: &sentry::Client) -> TauriPlugin<R> {
149 init_with_options(sentry_client, Default::default())
150}
151
152pub fn init_with_no_injection<R: Runtime>(sentry_client: &sentry::Client) -> TauriPlugin<R> {
153 init_with_options(
154 sentry_client,
155 Options {
156 javascript: JavaScriptOptions::no_injection(),
157 },
158 )
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn integration_adds_sdk_metadata() {
167 let options = ClientOptions::new().add_integration(Sentry::new());
168 let events = sentry::test::with_captured_events_options(
169 || {
170 sentry::capture_message("test", sentry::Level::Info);
171 },
172 options,
173 );
174
175 let sdk = events[0].sdk.as_ref().unwrap();
176 assert!(sdk.integrations.iter().any(|i| i == "tauri"));
177 assert!(sdk
178 .packages
179 .iter()
180 .any(|p| p.name == "cargo:tauri-plugin-sentry"));
181 }
182}