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