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