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}