Skip to main content

tauri_plugin_auditaur/
lib.rs

1pub mod commands;
2pub mod desktop;
3pub mod error;
4pub mod ipc;
5pub mod state;
6pub mod tracing;
7
8use auditaur_core::model::TauriWindowState;
9pub use auditaur_core::AuditaurConfig;
10pub use ipc::{ipc_traceparent, IpcTraceContext, IPC_CONTEXT_ARG};
11use serde_json::{json, Map, Value};
12use tauri::{
13    plugin::{Builder as TauriPluginBuilder, TauriPlugin},
14    Manager, Runtime, WebviewWindow, Window, WindowEvent,
15};
16pub use tauri_plugin_auditaur_macros::instrument_ipc;
17pub use tracing::tracing_layer;
18
19#[cfg(test)]
20pub(crate) mod test_support {
21    use std::sync::{Mutex, MutexGuard};
22
23    static GLOBAL_STATE_LOCK: Mutex<()> = Mutex::new(());
24
25    pub(crate) fn global_state_lock() -> MutexGuard<'static, ()> {
26        GLOBAL_STATE_LOCK.lock().unwrap()
27    }
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct Builder {
32    config: AuditaurConfig,
33}
34
35impl Builder {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    pub fn service_name(mut self, service_name: impl Into<String>) -> Self {
41        self.config.service_name = Some(service_name.into());
42        self
43    }
44
45    pub fn session_name(mut self, session_name: impl Into<String>) -> Self {
46        self.config.session_name = Some(session_name.into());
47        self
48    }
49
50    pub fn redact_defaults(mut self, redact_defaults: bool) -> Self {
51        self.config.redact_defaults = redact_defaults;
52        self
53    }
54
55    pub fn max_session_bytes(mut self, max_session_bytes: u64) -> Self {
56        self.config.max_session_bytes = max_session_bytes;
57        self
58    }
59
60    pub fn allow_release_builds(mut self, allow_release_builds: bool) -> Self {
61        self.config.allow_release_builds = allow_release_builds;
62        self
63    }
64
65    pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
66        let config = self.config;
67        TauriPluginBuilder::new("auditaur")
68            .invoke_handler(tauri::generate_handler![commands::export_otel_batch])
69            .on_window_ready(|window| {
70                record_window_ready(&window);
71                register_window_lifecycle(window);
72            })
73            .setup(move |app, _api| {
74                let app_identifier = Some(app.config().identifier.clone());
75                let state = state::AuditaurState::initialize(
76                    config.clone(),
77                    std::process::id(),
78                    app_identifier,
79                )?;
80                capture_initial_windows(app, &state);
81                app.manage(state);
82                Ok(())
83            })
84            .build()
85    }
86}
87
88fn capture_initial_windows<R: Runtime>(app: &tauri::AppHandle<R>, state: &state::AuditaurState) {
89    let Some(session_id) = state.session_id.as_ref() else {
90        return;
91    };
92    let Some(store) = state.store() else {
93        return;
94    };
95    let Ok(store) = store.lock() else {
96        return;
97    };
98    for window in app.webview_windows().values() {
99        let record = window_state(session_id, window);
100        let _ = store.insert_tauri_window_state(&record);
101    }
102}
103
104fn register_window_lifecycle<R: Runtime>(window: Window<R>) {
105    let listener_window = window.clone();
106    window.on_window_event(move |event| record_window_event(&listener_window, event));
107}
108
109fn record_window_ready<R: Runtime>(window: &Window<R>) {
110    record_window_state(window, "window_ready", None);
111}
112
113fn record_window_event<R: Runtime>(window: &Window<R>, event: &WindowEvent) {
114    record_window_state(window, "window_event", Some(event));
115}
116
117fn record_window_state<R: Runtime>(window: &Window<R>, capture: &str, event: Option<&WindowEvent>) {
118    let Some(state) = window.try_state::<state::AuditaurState>() else {
119        return;
120    };
121    let Some(session_id) = state.session_id.as_ref() else {
122        return;
123    };
124    let Some(store) = state.store() else {
125        return;
126    };
127    let Ok(store) = store.lock() else {
128        return;
129    };
130    let size = window.inner_size().ok();
131    let attributes = window_attributes(capture, event);
132    let record = TauriWindowState {
133        session_id: session_id.to_string(),
134        timestamp_unix_nanos: now_unix_nanos(),
135        window_label: window.label().to_string(),
136        webview_label: None,
137        url: None,
138        title: window.title().ok(),
139        focused: window.is_focused().ok(),
140        visible: window.is_visible().ok(),
141        width: size.as_ref().map(|size| f64::from(size.width)),
142        height: size.as_ref().map(|size| f64::from(size.height)),
143        scale_factor: window.scale_factor().ok(),
144        attributes,
145    };
146    let _ = store.insert_tauri_window_state(&record);
147}
148
149fn window_attributes(capture: &str, event: Option<&WindowEvent>) -> Value {
150    let mut attributes = Map::new();
151    attributes.insert("auditaur.capture".to_string(), json!(capture));
152
153    if let Some(event) = event {
154        attributes.extend(window_event_attributes(event));
155    }
156
157    Value::Object(attributes)
158}
159
160fn window_event_attributes(event: &WindowEvent) -> Map<String, Value> {
161    let mut attributes = Map::new();
162    attributes.insert(
163        "tauri.window.event".to_string(),
164        json!(window_event_kind(event)),
165    );
166    attributes.insert(
167        "tauri.window.event_debug".to_string(),
168        json!(format!("{event:?}")),
169    );
170
171    match event {
172        WindowEvent::Resized(size) => {
173            attributes.insert("tauri.window.event.width".to_string(), json!(size.width));
174            attributes.insert("tauri.window.event.height".to_string(), json!(size.height));
175        }
176        WindowEvent::Moved(position) => {
177            attributes.insert("tauri.window.event.x".to_string(), json!(position.x));
178            attributes.insert("tauri.window.event.y".to_string(), json!(position.y));
179        }
180        WindowEvent::Focused(focused) => {
181            attributes.insert("tauri.window.event.focused".to_string(), json!(focused));
182        }
183        WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
184            attributes.insert(
185                "tauri.window.event.scale_factor".to_string(),
186                json!(scale_factor),
187            );
188        }
189        WindowEvent::ThemeChanged(theme) => {
190            attributes.insert(
191                "tauri.window.event.theme".to_string(),
192                json!(format!("{theme:?}")),
193            );
194        }
195        _ => {}
196    }
197
198    attributes
199}
200
201fn window_event_kind(event: &WindowEvent) -> &'static str {
202    match event {
203        WindowEvent::Resized(_) => "resized",
204        WindowEvent::Moved(_) => "moved",
205        WindowEvent::CloseRequested { .. } => "close_requested",
206        WindowEvent::Destroyed => "destroyed",
207        WindowEvent::Focused(true) => "focused",
208        WindowEvent::Focused(false) => "blurred",
209        WindowEvent::ScaleFactorChanged { .. } => "scale_factor_changed",
210        WindowEvent::DragDrop(_) => "drag_drop",
211        WindowEvent::ThemeChanged(_) => "theme_changed",
212        _ => "unknown",
213    }
214}
215
216fn window_state<R: Runtime>(session_id: &str, window: &WebviewWindow<R>) -> TauriWindowState {
217    let size = window.inner_size().ok();
218    TauriWindowState {
219        session_id: session_id.to_string(),
220        timestamp_unix_nanos: now_unix_nanos(),
221        window_label: window.label().to_string(),
222        webview_label: Some(window.label().to_string()),
223        url: None,
224        title: window.title().ok(),
225        focused: window.is_focused().ok(),
226        visible: window.is_visible().ok(),
227        width: size.as_ref().map(|size| f64::from(size.width)),
228        height: size.as_ref().map(|size| f64::from(size.height)),
229        scale_factor: window.scale_factor().ok(),
230        attributes: json!({ "auditaur.capture": "initial_window_state" }),
231    }
232}
233
234fn now_unix_nanos() -> i64 {
235    let now = std::time::SystemTime::now()
236        .duration_since(std::time::UNIX_EPOCH)
237        .unwrap_or_default();
238    i64::try_from(now.as_nanos()).unwrap_or(i64::MAX)
239}
240
241#[cfg(test)]
242mod window_tests {
243    use super::{window_attributes, window_event_attributes};
244
245    #[test]
246    fn focused_window_events_record_authoritative_event_state() {
247        let attributes = window_event_attributes(&tauri::WindowEvent::Focused(false));
248
249        assert_eq!(attributes["tauri.window.event"], "blurred");
250        assert_eq!(attributes["tauri.window.event.focused"], false);
251    }
252
253    #[test]
254    fn resize_window_events_record_authoritative_event_size() {
255        let attributes = window_event_attributes(&tauri::WindowEvent::Resized(
256            tauri::PhysicalSize::new(800, 600),
257        ));
258
259        assert_eq!(attributes["tauri.window.event"], "resized");
260        assert_eq!(attributes["tauri.window.event.width"], 800);
261        assert_eq!(attributes["tauri.window.event.height"], 600);
262    }
263
264    #[test]
265    fn moved_window_events_record_authoritative_event_position() {
266        let attributes = window_event_attributes(&tauri::WindowEvent::Moved(
267            tauri::PhysicalPosition::new(12, 34),
268        ));
269
270        assert_eq!(attributes["tauri.window.event"], "moved");
271        assert_eq!(attributes["tauri.window.event.x"], 12);
272        assert_eq!(attributes["tauri.window.event.y"], 34);
273    }
274
275    #[test]
276    fn capture_only_window_attributes_do_not_claim_an_event() {
277        let attributes = window_attributes("window_ready", None);
278
279        assert_eq!(attributes["auditaur.capture"], "window_ready");
280        assert!(attributes.get("tauri.window.event").is_none());
281    }
282}