Skip to main content

forge_core/config/
signals.rs

1//! Signals configuration for product analytics and diagnostics.
2
3use serde::{Deserialize, Serialize};
4
5/// Signals configuration for built-in product analytics and frontend diagnostics.
6///
7/// Captures user behavior, acquisition channels, feature usage, and frontend
8/// errors without cookies or persistent client-side state (GDPR-compliant).
9///
10/// ```toml
11/// [signals]
12/// enabled = true
13/// auto_capture = true
14/// diagnostics = true
15/// session_timeout_mins = 30
16/// retention_days = 90
17/// ```
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SignalsConfig {
20    /// Enable the signals pipeline (event ingestion, auto-capture, dashboards).
21    #[serde(default = "default_true")]
22    pub enabled: bool,
23
24    /// Auto-capture RPC calls as events without user code.
25    #[serde(default = "default_true")]
26    pub auto_capture: bool,
27
28    /// Capture frontend errors, failed RPCs, and breadcrumbs for reproduction.
29    #[serde(default = "default_true")]
30    pub diagnostics: bool,
31
32    /// Inactivity timeout before closing a session (minutes).
33    #[serde(default = "default_session_timeout_mins")]
34    pub session_timeout_mins: u32,
35
36    /// Days to retain event data before partition cleanup.
37    #[serde(default = "default_retention_days")]
38    pub retention_days: u32,
39
40    /// Hash client IP + UA into a daily-rotating visitor ID instead of storing raw IP.
41    #[serde(default)]
42    pub anonymize_ip: bool,
43
44    /// Max events per batch INSERT.
45    #[serde(default = "default_batch_size")]
46    pub batch_size: usize,
47
48    /// Max milliseconds between flushes of the event buffer.
49    #[serde(default = "default_flush_interval_ms")]
50    pub flush_interval_ms: u64,
51
52    /// Function names to exclude from auto-capture (exact match).
53    #[serde(default)]
54    pub excluded_functions: Vec<String>,
55
56    /// Tag bot traffic via UA detection (visible in dashboard filter).
57    #[serde(default = "default_true")]
58    pub bot_detection: bool,
59}
60
61impl Default for SignalsConfig {
62    fn default() -> Self {
63        Self {
64            enabled: true,
65            auto_capture: true,
66            diagnostics: true,
67            session_timeout_mins: default_session_timeout_mins(),
68            retention_days: default_retention_days(),
69            anonymize_ip: false,
70            batch_size: default_batch_size(),
71            flush_interval_ms: default_flush_interval_ms(),
72            excluded_functions: Vec::new(),
73            bot_detection: true,
74        }
75    }
76}
77
78use super::default_true;
79
80fn default_session_timeout_mins() -> u32 {
81    30
82}
83
84fn default_retention_days() -> u32 {
85    90
86}
87
88fn default_batch_size() -> usize {
89    100
90}
91
92fn default_flush_interval_ms() -> u64 {
93    5000
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)]
98mod tests {
99    use super::*;
100
101    #[tokio::test]
102    async fn default_config_has_correct_values() {
103        let config = SignalsConfig::default();
104        assert!(config.enabled);
105        assert!(config.auto_capture);
106        assert!(config.diagnostics);
107        assert_eq!(config.session_timeout_mins, 30);
108        assert_eq!(config.retention_days, 90);
109        assert!(!config.anonymize_ip);
110        assert_eq!(config.batch_size, 100);
111        assert_eq!(config.flush_interval_ms, 5000);
112        assert!(config.excluded_functions.is_empty());
113        assert!(config.bot_detection);
114    }
115
116    #[tokio::test]
117    async fn deserializes_empty_toml() {
118        #[derive(Deserialize)]
119        struct Wrapper {
120            #[serde(default)]
121            signals: SignalsConfig,
122        }
123
124        let from_empty: SignalsConfig = toml::from_str("").unwrap();
125        let from_table: Wrapper = toml::from_str("[signals]").unwrap();
126
127        for config in [from_empty, from_table.signals] {
128            assert!(config.enabled);
129            assert!(config.auto_capture);
130            assert!(config.diagnostics);
131            assert_eq!(config.session_timeout_mins, 30);
132            assert_eq!(config.retention_days, 90);
133            assert!(!config.anonymize_ip);
134            assert_eq!(config.batch_size, 100);
135            assert_eq!(config.flush_interval_ms, 5000);
136            assert!(config.excluded_functions.is_empty());
137            assert!(config.bot_detection);
138        }
139    }
140
141    #[tokio::test]
142    async fn partial_override_preserves_defaults() {
143        let config: SignalsConfig = toml::from_str("enabled = false").unwrap();
144        assert!(!config.enabled);
145        assert!(config.auto_capture);
146        assert!(config.diagnostics);
147        assert_eq!(config.session_timeout_mins, 30);
148        assert_eq!(config.retention_days, 90);
149        assert!(!config.anonymize_ip);
150        assert_eq!(config.batch_size, 100);
151        assert_eq!(config.flush_interval_ms, 5000);
152        assert!(config.excluded_functions.is_empty());
153        assert!(config.bot_detection);
154    }
155
156    #[tokio::test]
157    async fn excluded_functions_from_toml() {
158        let config: SignalsConfig =
159            toml::from_str(r#"excluded_functions = ["health_check", "readiness"]"#).unwrap();
160        assert_eq!(config.excluded_functions, vec!["health_check", "readiness"]);
161    }
162
163    #[tokio::test]
164    async fn all_fields_overridden() {
165        let toml_str = r#"
166            enabled = false
167            auto_capture = false
168            diagnostics = false
169            session_timeout_mins = 60
170            retention_days = 30
171            anonymize_ip = true
172            batch_size = 500
173            flush_interval_ms = 10000
174            excluded_functions = ["ping"]
175            bot_detection = false
176        "#;
177        let config: SignalsConfig = toml::from_str(toml_str).unwrap();
178        assert!(!config.enabled);
179        assert!(!config.auto_capture);
180        assert!(!config.diagnostics);
181        assert_eq!(config.session_timeout_mins, 60);
182        assert_eq!(config.retention_days, 30);
183        assert!(config.anonymize_ip);
184        assert_eq!(config.batch_size, 500);
185        assert_eq!(config.flush_interval_ms, 10000);
186        assert_eq!(config.excluded_functions, vec!["ping"]);
187        assert!(!config.bot_detection);
188    }
189}