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    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/// A Tauri plugin that is also a Sentry integration.
60///
61/// Add a clone to the Sentry `ClientOptions` before `sentry::init` and
62/// pass the original to `tauri::Builder::plugin`.
63#[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}