Skip to main content

leptos_next_metadata/analytics/
handlers.rs

1//! Analytics Event Handlers
2//!
3//! Provides various handlers for processing analytics events including
4//! console logging, local storage, and remote analytics services.
5
6use crate::analytics::{AnalyticsEvent, AnalyticsEventHandler};
7use crate::error::{ErrorKind, MetadataError};
8use serde::{Deserialize, Serialize};
9
10/// Console analytics handler for development and debugging
11pub struct ConsoleAnalyticsHandler {
12    /// Handler name
13    name: String,
14    /// Log level
15    log_level: LogLevel,
16}
17
18/// Log levels for console output
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub enum LogLevel {
21    Debug,
22    Info,
23    Warn,
24    Error,
25}
26
27impl ConsoleAnalyticsHandler {
28    /// Create a new console analytics handler
29    pub fn new(name: String, log_level: LogLevel) -> Self {
30        Self { name, log_level }
31    }
32
33    /// Create a default console handler
34    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        // Log additional properties if available
54        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/// Local storage analytics handler for browser environments
89#[cfg(target_arch = "wasm32")]
90pub struct LocalStorageAnalyticsHandler {
91    /// Handler name
92    name: String,
93    /// Storage key prefix
94    storage_prefix: String,
95    /// Maximum events to store
96    max_events: usize,
97}
98
99#[cfg(target_arch = "wasm32")]
100impl LocalStorageAnalyticsHandler {
101    /// Create a new local storage analytics handler
102    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    /// Create a default local storage handler
111    pub fn default() -> Self {
112        Self::new(
113            "local_storage".to_string(),
114            "leptos_analytics".to_string(),
115            1000,
116        )
117    }
118
119    /// Get events from local storage
120    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    /// Store events in local storage
160    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        // Limit the number of stored events
198        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        // Limit the number of stored events
212        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
226/// Remote analytics handler for sending events to external services
227pub struct RemoteAnalyticsHandler {
228    /// Handler name
229    name: String,
230    /// Endpoint URL
231    endpoint_url: String,
232    /// API key
233    api_key: Option<String>,
234    /// Request timeout in seconds
235    timeout_seconds: u64,
236    /// Batch size for remote requests
237    batch_size: usize,
238}
239
240impl RemoteAnalyticsHandler {
241    /// Create a new remote analytics handler
242    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    /// Create a handler for Google Analytics 4
259    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    /// Create a handler for custom analytics endpoint
274    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        // For single events, we'll batch them with a batch of 1
282        self.handle_batch(&[event.clone()])
283    }
284
285    fn handle_batch(&self, events: &[AnalyticsEvent]) -> Result<(), MetadataError> {
286        // In a real implementation, this would make HTTP requests
287        // For now, we'll simulate the behavior
288
289        if events.is_empty() {
290            return Ok(());
291        }
292
293        // Simulate network request
294        println!(
295            "[Analytics] {} - Sending {} events to {}",
296            self.name,
297            events.len(),
298            self.endpoint_url
299        );
300
301        // In a real implementation, you would:
302        // 1. Serialize events to JSON
303        // 2. Create HTTP request with proper headers
304        // 3. Send request with timeout
305        // 4. Handle response and errors
306
307        // For now, just log the events
308        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/// File-based analytics handler for native environments
321#[cfg(not(target_arch = "wasm32"))]
322pub struct FileAnalyticsHandler {
323    /// Handler name
324    name: String,
325    /// File path
326    file_path: String,
327    /// Maximum file size in bytes
328    max_file_size: usize,
329}
330
331#[cfg(not(target_arch = "wasm32"))]
332impl FileAnalyticsHandler {
333    /// Create a new file analytics handler
334    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    /// Create a default file handler
343    pub fn default() -> Self {
344        Self::new(
345            "file".to_string(),
346            "analytics_events.jsonl".to_string(),
347            10 * 1024 * 1024, // 10MB
348        )
349    }
350
351    /// Append events to file
352    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    /// Check if file needs rotation
373    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), // File doesn't exist, no rotation needed
379        }
380    }
381
382    /// Rotate the log file
383    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        // Check if file rotation is needed
404        self.rotate_file()?;
405
406        // Append events to file
407        self.append_events(events)?;
408
409        Ok(())
410    }
411
412    fn name(&self) -> &str {
413        &self.name
414    }
415}
416
417/// Memory-based analytics handler for temporary storage
418pub struct MemoryAnalyticsHandler {
419    /// Handler name
420    name: String,
421    /// Stored events
422    events: std::sync::Mutex<Vec<AnalyticsEvent>>,
423    /// Maximum events to store
424    max_events: usize,
425}
426
427impl MemoryAnalyticsHandler {
428    /// Create a new memory analytics handler
429    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    /// Create a default memory handler
438    pub fn default() -> Self {
439        Self::new("memory".to_string(), 1000)
440    }
441
442    /// Get all stored events
443    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    /// Clear all stored events
451    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    /// Get event count
460    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        // Limit the number of stored events
481        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
494/// Analytics handler factory for creating handlers
495pub struct AnalyticsHandlerFactory;
496
497impl AnalyticsHandlerFactory {
498    /// Create a console handler
499    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    /// Create a memory handler
510    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    /// Create a local storage handler (WASM only)
521    #[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    /// Create a file handler (native only)
535    #[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    /// Create a remote handler
549    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    /// Create a Google Analytics 4 handler
564    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}