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}