telemetry_kit/sync/
config.rs1use crate::error::{Result, TelemetryError};
4use uuid::Uuid;
5
6pub const DEFAULT_ENDPOINT: &str = "https://telemetry-kit.dev";
8
9pub const STAGING_ENDPOINT: &str = "https://staging.telemetry-kit.dev";
11
12pub const MAX_BATCH_SIZE: usize = 1000;
14
15pub const DEFAULT_BATCH_SIZE: usize = 100;
17
18#[derive(Debug, Clone)]
20pub struct SyncConfig {
21 pub endpoint: String,
23
24 pub org_id: Uuid,
26
27 pub app_id: Uuid,
29
30 pub token: String,
32
33 pub secret: String,
35
36 pub batch_size: usize,
38
39 pub max_retries: u32,
41
42 pub sync_interval_secs: u64,
44
45 pub respect_dnt: bool,
47}
48
49impl SyncConfig {
50 pub fn builder() -> SyncConfigBuilder {
52 SyncConfigBuilder::new()
53 }
54
55 pub fn ingestion_url(&self) -> String {
57 format!(
58 "{}/v1/ingest/{}/{}",
59 self.endpoint, self.org_id, self.app_id
60 )
61 }
62
63 pub fn validate(&self) -> Result<()> {
65 if self.token.is_empty() {
66 return Err(TelemetryError::invalid_config(
67 "token",
68 "Token cannot be empty. Generate one at telemetry-kit.dev/settings/tokens",
69 ));
70 }
71
72 if self.secret.is_empty() {
73 return Err(TelemetryError::invalid_config(
74 "secret",
75 "Secret cannot be empty. Copy it from telemetry-kit.dev/settings/tokens",
76 ));
77 }
78
79 if self.batch_size == 0 || self.batch_size > MAX_BATCH_SIZE {
80 return Err(TelemetryError::invalid_config(
81 "batch_size",
82 &format!(
83 "Must be between 1 and {} (got {})",
84 MAX_BATCH_SIZE, self.batch_size
85 ),
86 ));
87 }
88
89 Ok(())
90 }
91}
92
93#[derive(Debug, Default)]
95pub struct SyncConfigBuilder {
96 endpoint: Option<String>,
97 org_id: Option<Uuid>,
98 app_id: Option<Uuid>,
99 token: Option<String>,
100 secret: Option<String>,
101 batch_size: Option<usize>,
102 max_retries: Option<u32>,
103 sync_interval_secs: Option<u64>,
104 respect_dnt: Option<bool>,
105}
106
107impl SyncConfigBuilder {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
115 self.endpoint = Some(endpoint.into());
116 self
117 }
118
119 pub fn use_staging(mut self) -> Self {
121 self.endpoint = Some(STAGING_ENDPOINT.to_string());
122 self
123 }
124
125 pub fn org_id(mut self, org_id: impl Into<String>) -> Result<Self> {
127 let org_id_str = org_id.into();
128 let uuid = Uuid::parse_str(&org_id_str)
129 .map_err(|_| TelemetryError::invalid_uuid("org_id", &org_id_str))?;
130 self.org_id = Some(uuid);
131 Ok(self)
132 }
133
134 pub fn org_id_uuid(mut self, org_id: Uuid) -> Self {
136 self.org_id = Some(org_id);
137 self
138 }
139
140 pub fn app_id(mut self, app_id: impl Into<String>) -> Result<Self> {
142 let app_id_str = app_id.into();
143 let uuid = Uuid::parse_str(&app_id_str)
144 .map_err(|_| TelemetryError::invalid_uuid("app_id", &app_id_str))?;
145 self.app_id = Some(uuid);
146 Ok(self)
147 }
148
149 pub fn app_id_uuid(mut self, app_id: Uuid) -> Self {
151 self.app_id = Some(app_id);
152 self
153 }
154
155 pub fn token(mut self, token: impl Into<String>) -> Self {
157 self.token = Some(token.into());
158 self
159 }
160
161 pub fn secret(mut self, secret: impl Into<String>) -> Self {
163 self.secret = Some(secret.into());
164 self
165 }
166
167 pub fn batch_size(mut self, batch_size: usize) -> Self {
169 self.batch_size = Some(batch_size);
170 self
171 }
172
173 pub fn max_retries(mut self, max_retries: u32) -> Self {
175 self.max_retries = Some(max_retries);
176 self
177 }
178
179 pub fn sync_interval_secs(mut self, interval: u64) -> Self {
181 self.sync_interval_secs = Some(interval);
182 self
183 }
184
185 pub fn respect_dnt(mut self, respect: bool) -> Self {
187 self.respect_dnt = Some(respect);
188 self
189 }
190
191 pub fn build(self) -> Result<SyncConfig> {
193 let config = SyncConfig {
194 endpoint: self
195 .endpoint
196 .unwrap_or_else(|| DEFAULT_ENDPOINT.to_string()),
197 org_id: self
198 .org_id
199 .ok_or_else(|| TelemetryError::missing_field("org_id"))?,
200 app_id: self
201 .app_id
202 .ok_or_else(|| TelemetryError::missing_field("app_id"))?,
203 token: self
204 .token
205 .ok_or_else(|| TelemetryError::missing_field("token"))?,
206 secret: self
207 .secret
208 .ok_or_else(|| TelemetryError::missing_field("secret"))?,
209 batch_size: self.batch_size.unwrap_or(DEFAULT_BATCH_SIZE),
210 max_retries: self.max_retries.unwrap_or(5),
211 sync_interval_secs: self.sync_interval_secs.unwrap_or(3600), respect_dnt: self.respect_dnt.unwrap_or(true),
213 };
214
215 config.validate()?;
216 Ok(config)
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn test_config_builder() {
226 let config = SyncConfig::builder()
227 .org_id("550e8400-e29b-41d4-a716-446655440000")
228 .unwrap()
229 .app_id("7c9e6679-7425-40de-944b-e07fc1f90ae7")
230 .unwrap()
231 .token("tk_test_token")
232 .secret("test_secret")
233 .build()
234 .unwrap();
235
236 assert_eq!(config.endpoint, DEFAULT_ENDPOINT);
237 assert_eq!(config.batch_size, DEFAULT_BATCH_SIZE);
238 assert!(config.respect_dnt);
239 }
240
241 #[test]
242 fn test_staging_endpoint() {
243 let config = SyncConfig::builder()
244 .use_staging()
245 .org_id("550e8400-e29b-41d4-a716-446655440000")
246 .unwrap()
247 .app_id("7c9e6679-7425-40de-944b-e07fc1f90ae7")
248 .unwrap()
249 .token("tk_test_token")
250 .secret("test_secret")
251 .build()
252 .unwrap();
253
254 assert_eq!(config.endpoint, STAGING_ENDPOINT);
255 }
256
257 #[test]
258 fn test_ingestion_url() {
259 let config = SyncConfig::builder()
260 .org_id("550e8400-e29b-41d4-a716-446655440000")
261 .unwrap()
262 .app_id("7c9e6679-7425-40de-944b-e07fc1f90ae7")
263 .unwrap()
264 .token("tk_test_token")
265 .secret("test_secret")
266 .build()
267 .unwrap();
268
269 let url = config.ingestion_url();
270 assert!(url.contains("/v1/ingest/"));
271 assert!(url.contains("550e8400-e29b-41d4-a716-446655440000"));
272 assert!(url.contains("7c9e6679-7425-40de-944b-e07fc1f90ae7"));
273 }
274
275 #[test]
276 fn test_validation_empty_token() {
277 let result = SyncConfig::builder()
278 .org_id("550e8400-e29b-41d4-a716-446655440000")
279 .unwrap()
280 .app_id("7c9e6679-7425-40de-944b-e07fc1f90ae7")
281 .unwrap()
282 .token("")
283 .secret("test_secret")
284 .build();
285
286 assert!(result.is_err());
287 }
288
289 #[test]
290 fn test_validation_invalid_batch_size() {
291 let result = SyncConfig::builder()
292 .org_id("550e8400-e29b-41d4-a716-446655440000")
293 .unwrap()
294 .app_id("7c9e6679-7425-40de-944b-e07fc1f90ae7")
295 .unwrap()
296 .token("tk_test_token")
297 .secret("test_secret")
298 .batch_size(2000) .build();
300
301 assert!(result.is_err());
302 }
303}