1use crate::analytics::{AnalyticsEvent, AnalyticsEventHandler};
7use crate::error::{ErrorKind, MetadataError};
8use serde::{Deserialize, Serialize};
9
10pub struct ConsoleAnalyticsHandler {
12 name: String,
14 log_level: LogLevel,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum LogLevel {
21 Debug,
22 Info,
23 Warn,
24 Error,
25}
26
27impl ConsoleAnalyticsHandler {
28 pub fn new(name: String, log_level: LogLevel) -> Self {
30 Self { name, log_level }
31 }
32
33 pub fn default() -> Self {
35 Self::new("console".to_string(), LogLevel::Info)
36 }
37}
38
39impl AnalyticsEventHandler for ConsoleAnalyticsHandler {
40 fn handle_event(&self, event: &AnalyticsEvent) -> Result<(), MetadataError> {
41 let log_message = format!(
42 "[Analytics] {} - {} at {}",
43 self.name, event.event_type, event.timestamp
44 );
45
46 match self.log_level {
47 LogLevel::Debug => println!("DEBUG: {}", log_message),
48 LogLevel::Info => println!("INFO: {}", log_message),
49 LogLevel::Warn => println!("WARN: {}", log_message),
50 LogLevel::Error => eprintln!("ERROR: {}", log_message),
51 }
52
53 if !event.properties.is_empty() {
55 println!(" Properties: {:?}", event.properties);
56 }
57
58 if let Some(duration) = event.duration_ms {
59 println!(" Duration: {}ms", duration);
60 }
61
62 if let Some(error) = &event.error {
63 println!(" Error: {} - {}", error.kind, error.message);
64 }
65
66 Ok(())
67 }
68
69 fn handle_batch(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
70 println!(
71 "[Analytics] {} - Processing batch of {} events",
72 self.name,
73 events.len()
74 );
75
76 for event in events {
77 self.handle_event(event)?;
78 }
79
80 Ok(())
81 }
82
83 fn name(&self) -> &str {
84 &self.name
85 }
86}
87
88#[cfg(target_arch = "wasm32")]
90pub struct LocalStorageAnalyticsHandler {
91 name: String,
93 storage_prefix: String,
95 max_events: usize,
97}
98
99#[cfg(target_arch = "wasm32")]
100impl LocalStorageAnalyticsHandler {
101 pub fn new(name: String, storage_prefix: String, max_events: usize) -> Self {
103 Self {
104 name,
105 storage_prefix,
106 max_events,
107 }
108 }
109
110 pub fn default() -> Self {
112 Self::new(
113 "local_storage".to_string(),
114 "leptos_analytics".to_string(),
115 1000,
116 )
117 }
118
119 fn get_stored_events(&self) -> Result<Vec<AnalyticsEvent>, MetadataError> {
121 use web_sys::window;
122
123 let window = window().ok_or_else(|| {
124 MetadataError::new(ErrorKind::Browser, "Window not available".to_string())
125 })?;
126
127 let storage = window
128 .local_storage()
129 .map_err(|_| {
130 MetadataError::new(
131 ErrorKind::Browser,
132 "Local storage not available".to_string(),
133 )
134 })?
135 .ok_or_else(|| {
136 MetadataError::new(
137 ErrorKind::Browser,
138 "Local storage not available".to_string(),
139 )
140 })?;
141
142 let key = format!("{}_events", self.storage_prefix);
143 let stored_data = storage.get_item(&key).map_err(|_| {
144 MetadataError::new(
145 ErrorKind::Browser,
146 "Failed to read from local storage".to_string(),
147 )
148 })?;
149
150 if let Some(data) = stored_data {
151 let events: Vec<AnalyticsEvent> = serde_json::from_str(&data)
152 .map_err(|e| MetadataError::new(ErrorKind::Serialization, e.to_string()))?;
153 Ok(events)
154 } else {
155 Ok(Vec::new())
156 }
157 }
158
159 fn store_events(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
161 use web_sys::window;
162
163 let window = window().ok_or_else(|| {
164 MetadataError::new(ErrorKind::Browser, "Window not available".to_string())
165 })?;
166
167 let storage = window.local_storage().map_err(|_| {
168 MetadataError::new(
169 ErrorKind::Browser,
170 "Local storage not available".to_string(),
171 )
172 })?;
173
174 if let Some(storage) = storage {
175 let key = format!("{}_events", self.storage_prefix);
176 let data = serde_json::to_string(events)
177 .map_err(|e| MetadataError::new(ErrorKind::Serialization, e.to_string()))?;
178
179 storage.set_item(&key, &data).map_err(|_| {
180 MetadataError::new(
181 ErrorKind::Browser,
182 "Failed to write to local storage".to_string(),
183 )
184 })?;
185 }
186
187 Ok(())
188 }
189}
190
191#[cfg(target_arch = "wasm32")]
192impl AnalyticsEventHandler for LocalStorageAnalyticsHandler {
193 fn handle_event(&self, event: &AnalyticsEvent) -> Result<(), MetadataError> {
194 let mut stored_events = self.get_stored_events().unwrap_or_default();
195 stored_events.push(event.clone());
196
197 if stored_events.len() > self.max_events {
199 let excess = stored_events.len() - self.max_events;
200 stored_events.drain(0..excess);
201 }
202
203 self.store_events(&stored_events)?;
204 Ok(())
205 }
206
207 fn handle_batch(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
208 let mut stored_events = self.get_stored_events().unwrap_or_default();
209 stored_events.extend(events.iter().cloned());
210
211 if stored_events.len() > self.max_events {
213 let excess = stored_events.len() - self.max_events;
214 stored_events.drain(0..excess);
215 }
216
217 self.store_events(&stored_events)?;
218 Ok(())
219 }
220
221 fn name(&self) -> &str {
222 &self.name
223 }
224}
225
226pub struct RemoteAnalyticsHandler {
228 name: String,
230 endpoint_url: String,
232 api_key: Option<String>,
234 timeout_seconds: u64,
236 batch_size: usize,
238}
239
240impl RemoteAnalyticsHandler {
241 pub fn new(
243 name: String,
244 endpoint_url: String,
245 api_key: Option<String>,
246 timeout_seconds: u64,
247 batch_size: usize,
248 ) -> Self {
249 Self {
250 name,
251 endpoint_url,
252 api_key,
253 timeout_seconds,
254 batch_size,
255 }
256 }
257
258 pub fn google_analytics_4(measurement_id: String, api_secret: Option<String>) -> Self {
260 let endpoint = format!(
261 "https://www.google-analytics.com/mp/collect?measurement_id={}",
262 measurement_id
263 );
264 Self::new(
265 "google_analytics_4".to_string(),
266 endpoint,
267 api_secret,
268 10,
269 20,
270 )
271 }
272
273 pub fn custom_endpoint(name: String, endpoint_url: String, api_key: Option<String>) -> Self {
275 Self::new(name, endpoint_url, api_key, 10, 10)
276 }
277}
278
279impl AnalyticsEventHandler for RemoteAnalyticsHandler {
280 fn handle_event(&self, event: &AnalyticsEvent) -> Result<(), MetadataError> {
281 self.handle_batch(&[event.clone()])
283 }
284
285 fn handle_batch(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
286 if events.is_empty() {
290 return Ok(());
291 }
292
293 println!(
295 "[Analytics] {} - Sending {} events to {}",
296 self.name,
297 events.len(),
298 self.endpoint_url
299 );
300
301 for event in events {
309 println!(" Event: {} at {}", event.event_type, event.timestamp);
310 }
311
312 Ok(())
313 }
314
315 fn name(&self) -> &str {
316 &self.name
317 }
318}
319
320#[cfg(not(target_arch = "wasm32"))]
322pub struct FileAnalyticsHandler {
323 name: String,
325 file_path: String,
327 max_file_size: usize,
329}
330
331#[cfg(not(target_arch = "wasm32"))]
332impl FileAnalyticsHandler {
333 pub fn new(name: String, file_path: String, max_file_size: usize) -> Self {
335 Self {
336 name,
337 file_path,
338 max_file_size,
339 }
340 }
341
342 pub fn default() -> Self {
344 Self::new(
345 "file".to_string(),
346 "analytics_events.jsonl".to_string(),
347 10 * 1024 * 1024, )
349 }
350
351 fn append_events(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
353 use std::fs::OpenOptions;
354 use std::io::Write;
355
356 let mut file = OpenOptions::new()
357 .create(true)
358 .append(true)
359 .open(&self.file_path)
360 .map_err(|e| MetadataError::new(ErrorKind::FileSystem, e.to_string()))?;
361
362 for event in events {
363 let json_line = serde_json::to_string(event)
364 .map_err(|e| MetadataError::new(ErrorKind::Serialization, e.to_string()))?;
365 writeln!(file, "{}", json_line)
366 .map_err(|e| MetadataError::new(ErrorKind::FileSystem, e.to_string()))?;
367 }
368
369 Ok(())
370 }
371
372 fn needs_rotation(&self) -> Result<bool, MetadataError> {
374 use std::fs::metadata;
375
376 match metadata(&self.file_path) {
377 Ok(metadata) => Ok(metadata.len() as usize > self.max_file_size),
378 Err(_) => Ok(false), }
380 }
381
382 fn rotate_file(&self) -> Result<(), MetadataError> {
384 use std::fs;
385
386 if self.needs_rotation()? {
387 let backup_path = format!("{}.backup", self.file_path);
388 fs::rename(&self.file_path, &backup_path)
389 .map_err(|e| MetadataError::new(ErrorKind::FileSystem, e.to_string()))?;
390 }
391
392 Ok(())
393 }
394}
395
396#[cfg(not(target_arch = "wasm32"))]
397impl AnalyticsEventHandler for FileAnalyticsHandler {
398 fn handle_event(&self, event: &AnalyticsEvent) -> Result<(), MetadataError> {
399 self.handle_batch(&[event.clone()])
400 }
401
402 fn handle_batch(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
403 self.rotate_file()?;
405
406 self.append_events(events)?;
408
409 Ok(())
410 }
411
412 fn name(&self) -> &str {
413 &self.name
414 }
415}
416
417pub struct MemoryAnalyticsHandler {
419 name: String,
421 events: std::sync::Mutex<Vec<AnalyticsEvent>>,
423 max_events: usize,
425}
426
427impl MemoryAnalyticsHandler {
428 pub fn new(name: String, max_events: usize) -> Self {
430 Self {
431 name,
432 events: std::sync::Mutex::new(Vec::new()),
433 max_events,
434 }
435 }
436
437 pub fn default() -> Self {
439 Self::new("memory".to_string(), 1000)
440 }
441
442 pub fn get_events(&self) -> Result<Vec<AnalyticsEvent>, MetadataError> {
444 let events = self.events.lock().map_err(|_| {
445 MetadataError::new(ErrorKind::Unknown, "Failed to acquire lock".to_string())
446 })?;
447 Ok(events.clone())
448 }
449
450 pub fn clear_events(&self) -> Result<(), MetadataError> {
452 let mut events = self.events.lock().map_err(|_| {
453 MetadataError::new(ErrorKind::Unknown, "Failed to acquire lock".to_string())
454 })?;
455 events.clear();
456 Ok(())
457 }
458
459 pub fn event_count(&self) -> Result<usize, MetadataError> {
461 let events = self.events.lock().map_err(|_| {
462 MetadataError::new(ErrorKind::Unknown, "Failed to acquire lock".to_string())
463 })?;
464 Ok(events.len())
465 }
466}
467
468impl AnalyticsEventHandler for MemoryAnalyticsHandler {
469 fn handle_event(&self, event: &AnalyticsEvent) -> Result<(), MetadataError> {
470 self.handle_batch(&[event.clone()])
471 }
472
473 fn handle_batch(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
474 let mut stored_events = self.events.lock().map_err(|_| {
475 MetadataError::new(ErrorKind::Unknown, "Failed to acquire lock".to_string())
476 })?;
477
478 stored_events.extend(events.iter().cloned());
479
480 if stored_events.len() > self.max_events {
482 let excess = stored_events.len() - self.max_events;
483 stored_events.drain(0..excess);
484 }
485
486 Ok(())
487 }
488
489 fn name(&self) -> &str {
490 &self.name
491 }
492}
493
494pub struct AnalyticsHandlerFactory;
496
497impl AnalyticsHandlerFactory {
498 pub fn console(
500 name: Option<String>,
501 log_level: Option<LogLevel>,
502 ) -> Box<dyn AnalyticsEventHandler> {
503 Box::new(ConsoleAnalyticsHandler::new(
504 name.unwrap_or_else(|| "console".to_string()),
505 log_level.unwrap_or(LogLevel::Info),
506 ))
507 }
508
509 pub fn memory(
511 name: Option<String>,
512 max_events: Option<usize>,
513 ) -> Box<dyn AnalyticsEventHandler> {
514 Box::new(MemoryAnalyticsHandler::new(
515 name.unwrap_or_else(|| "memory".to_string()),
516 max_events.unwrap_or(1000),
517 ))
518 }
519
520 #[cfg(target_arch = "wasm32")]
522 pub fn local_storage(
523 name: Option<String>,
524 storage_prefix: Option<String>,
525 max_events: Option<usize>,
526 ) -> Box<dyn AnalyticsEventHandler> {
527 Box::new(LocalStorageAnalyticsHandler::new(
528 name.unwrap_or_else(|| "local_storage".to_string()),
529 storage_prefix.unwrap_or_else(|| "leptos_analytics".to_string()),
530 max_events.unwrap_or(1000),
531 ))
532 }
533
534 #[cfg(not(target_arch = "wasm32"))]
536 pub fn file(
537 name: Option<String>,
538 file_path: Option<String>,
539 max_file_size: Option<usize>,
540 ) -> Box<dyn AnalyticsEventHandler> {
541 Box::new(FileAnalyticsHandler::new(
542 name.unwrap_or_else(|| "file".to_string()),
543 file_path.unwrap_or_else(|| "analytics_events.jsonl".to_string()),
544 max_file_size.unwrap_or(10 * 1024 * 1024),
545 ))
546 }
547
548 pub fn remote(
550 name: String,
551 endpoint_url: String,
552 api_key: Option<String>,
553 ) -> Box<dyn AnalyticsEventHandler> {
554 Box::new(RemoteAnalyticsHandler::new(
555 name,
556 endpoint_url,
557 api_key,
558 10,
559 10,
560 ))
561 }
562
563 pub fn google_analytics_4(
565 measurement_id: String,
566 api_secret: Option<String>,
567 ) -> Box<dyn AnalyticsEventHandler> {
568 Box::new(RemoteAnalyticsHandler::google_analytics_4(
569 measurement_id,
570 api_secret,
571 ))
572 }
573}