edgee_components_runtime/data_collection/
payload.rs

1use chrono::{DateTime, Utc};
2use regex::Regex;
3use serde::Deserialize;
4use serde::Serialize;
5use std::collections::HashMap;
6use std::fmt;
7use tracing::error;
8
9use crate::config::ComponentDataManipulationRule;
10use crate::config::ComponentEventFilteringRuleCondition;
11use crate::config::DataCollectionComponents;
12
13pub type Dict = HashMap<String, serde_json::Value>;
14
15#[derive(Serialize, Debug, Clone, Default)]
16pub struct Event {
17    pub uuid: String,
18    pub timestamp: DateTime<Utc>,
19    #[serde(rename = "type")]
20    pub event_type: EventType,
21    pub data: Data,
22    pub context: Context,
23    #[serde(skip_serializing)]
24    pub components: Option<HashMap<String, bool>>,
25    pub from: Option<String>,
26    pub consent: Option<Consent>,
27}
28
29impl<'de> Deserialize<'de> for Event {
30    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
31    where
32        D: serde::Deserializer<'de>,
33    {
34        #[derive(Deserialize)]
35        struct EventHelper {
36            uuid: String,
37            timestamp: DateTime<Utc>,
38            #[serde(rename = "type")]
39            event_type: EventType,
40            #[serde(default)]
41            data: serde_json::Value,
42            #[serde(default)]
43            context: Context,
44            #[serde(default)]
45            components: Option<HashMap<String, bool>>,
46            #[serde(default)]
47            from: Option<String>,
48            #[serde(default)]
49            consent: Option<Consent>,
50        }
51
52        let helper = EventHelper::deserialize(deserializer)?;
53        let data = match helper.event_type {
54            EventType::Page => Data::Page(serde_json::from_value(helper.data).unwrap()),
55            EventType::User => Data::User(serde_json::from_value(helper.data).unwrap()),
56            EventType::Track => {
57                let mut track_data: Track = serde_json::from_value(helper.data).unwrap();
58
59                // If products exist in properties, move them to the products field
60                if let Some(serde_json::Value::Array(products_array)) =
61                    track_data.properties.remove("products")
62                {
63                    track_data.products = products_array
64                        .into_iter()
65                        .filter_map(|p| {
66                            if let serde_json::Value::Object(map) = p {
67                                Some(map.into_iter().collect())
68                            } else {
69                                None
70                            }
71                        })
72                        .collect();
73                }
74
75                Data::Track(track_data)
76            }
77        };
78
79        Ok(Event {
80            uuid: helper.uuid,
81            timestamp: helper.timestamp,
82            event_type: helper.event_type,
83            data,
84            context: helper.context,
85            components: helper.components,
86            from: helper.from,
87            consent: helper.consent,
88        })
89    }
90}
91
92impl Event {
93    pub fn is_component_enabled(&self, config: &DataCollectionComponents) -> &bool {
94        // if destinations is not set, return true
95        if self.components.is_none() {
96            return &true;
97        }
98
99        let components = self.components.as_ref().unwrap();
100
101        // get destinations.get("all")
102        let all = components.get("all").unwrap_or(&true);
103
104        // Check each possible key in order of priority
105        for key in [&config.id, &config.project_component_id, &config.slug] {
106            if let Some(enabled) = components.get(key.as_str()) {
107                return enabled;
108            }
109        }
110
111        all
112    }
113    pub fn should_filter_out(&self, condition: &ComponentEventFilteringRuleCondition) -> bool {
114        let query = condition.field.as_str();
115        let operator = condition.operator.as_str();
116        let value = condition.value.as_str();
117
118        let re = Regex::new(r"data\.(page|track|user)\.properties\.([a-zA-Z0-9_]+)").unwrap();
119        if let Some(captures) = re.captures(query) {
120            let data_type = captures.get(1).unwrap().as_str();
121            let custom_field = captures.get(2).unwrap().as_str();
122
123            match data_type {
124                "page" => {
125                    if let Data::Page(data) = &self.data {
126                        if let Some(found_customer_property_value) =
127                            data.properties.get(custom_field)
128                        {
129                            return evaluate_string_filter(
130                                found_customer_property_value.to_string().as_str(),
131                                operator,
132                                value,
133                            );
134                        }
135                    }
136                }
137                "track" => {
138                    if let Data::Track(data) = &self.data {
139                        if let Some(found_customer_property_value) =
140                            data.properties.get(custom_field)
141                        {
142                            return evaluate_string_filter(
143                                found_customer_property_value.to_string().as_str(),
144                                operator,
145                                value,
146                            );
147                        }
148                    }
149                }
150                "user" => {
151                    if let Data::User(data) = &self.data {
152                        if let Some(found_customer_property_value) =
153                            data.properties.get(custom_field)
154                        {
155                            return evaluate_string_filter(
156                                found_customer_property_value.to_string().as_str(),
157                                operator,
158                                value,
159                            );
160                        }
161                    }
162                }
163                _ => {}
164            }
165        }
166
167        match query {
168            "uuid" => evaluate_string_filter(&self.uuid, operator, value),
169            "timestamp" => {
170                let timestamp_f64 = self.timestamp.timestamp() as f64;
171                let value_f64 = value.parse::<f64>().unwrap_or_default();
172                evaluate_number_filter(&timestamp_f64, operator, &value_f64)
173            }
174            "timestamp-millis" => {
175                let timestamp_f64 = self.timestamp.timestamp_millis() as f64;
176                let value_f64 = value.parse::<f64>().unwrap_or_default();
177                evaluate_number_filter(&timestamp_f64, operator, &value_f64)
178            }
179            "timestamp-micros" => {
180                let timestamp_f64 = self.timestamp.timestamp_micros() as f64;
181                let value_f64 = value.parse::<f64>().unwrap_or_default();
182                evaluate_number_filter(&timestamp_f64, operator, &value_f64)
183            }
184            "event-type" => {
185                let event_type_str = match self.event_type {
186                    EventType::Page => "page",
187                    EventType::User => "user",
188                    EventType::Track => "track",
189                };
190                evaluate_string_filter(event_type_str, operator, value)
191            }
192            "consent" => {
193                let consent_str = self.consent.as_ref().map_or("", |c| match c {
194                    Consent::Granted => "granted",
195                    Consent::Denied => "denied",
196                    Consent::Pending => "pending",
197                });
198                evaluate_string_filter(consent_str, operator, value)
199            }
200            // Client fields
201            "context.client.ip" => evaluate_string_filter(&self.context.client.ip, operator, value),
202            "context.client.proxy-type" => {
203                let proxy_type_str = self.context.client.proxy_type.as_deref().unwrap_or("");
204                evaluate_string_filter(proxy_type_str, operator, value)
205            }
206            "context.client.proxy-desc" => {
207                let proxy_desc_str = self.context.client.proxy_desc.as_deref().unwrap_or("");
208                evaluate_string_filter(proxy_desc_str, operator, value)
209            }
210            "context.client.as-name" => {
211                let as_name_str = self.context.client.as_name.as_deref().unwrap_or("");
212                evaluate_string_filter(as_name_str, operator, value)
213            }
214            "context.client.as-number" => {
215                let as_number_f64 = self.context.client.as_number.unwrap_or(0) as f64;
216                let value_f64 = value.parse::<f64>().unwrap_or_default();
217                evaluate_number_filter(&as_number_f64, operator, &value_f64)
218            }
219            "context.client.locale" => {
220                evaluate_string_filter(&self.context.client.locale, operator, value)
221            }
222            "context.client.accept-language" => {
223                evaluate_string_filter(&self.context.client.accept_language, operator, value)
224            }
225            "context.client.timezone" => {
226                evaluate_string_filter(&self.context.client.timezone, operator, value)
227            }
228            "context.client.user-agent" => {
229                evaluate_string_filter(&self.context.client.user_agent, operator, value)
230            }
231            "context.client.user-agent-version-list" => evaluate_string_filter(
232                &self.context.client.user_agent_version_list,
233                operator,
234                value,
235            ),
236            "context.client.user-agent-mobile" => {
237                evaluate_string_filter(&self.context.client.user_agent_mobile, operator, value)
238            }
239            "context.client.os-name" => {
240                evaluate_string_filter(&self.context.client.os_name, operator, value)
241            }
242            "context.client.user-agent-architecture" => evaluate_string_filter(
243                &self.context.client.user_agent_architecture,
244                operator,
245                value,
246            ),
247            "context.client.user-agent-bitness" => {
248                evaluate_string_filter(&self.context.client.user_agent_bitness, operator, value)
249            }
250            "context.client.user-agent-full-version-list" => evaluate_string_filter(
251                &self.context.client.user_agent_full_version_list,
252                operator,
253                value,
254            ),
255            "context.client.user-agent-model" => {
256                evaluate_string_filter(&self.context.client.user_agent_model, operator, value)
257            }
258            "context.client.os-version" => {
259                evaluate_string_filter(&self.context.client.os_version, operator, value)
260            }
261            "context.client.screen-width" => {
262                let width_f64 = self.context.client.screen_width as f64;
263                let value_f64 = value.parse::<f64>().unwrap_or_default();
264                evaluate_number_filter(&width_f64, operator, &value_f64)
265            }
266            "context.client.screen-height" => {
267                let height_f64 = self.context.client.screen_height as f64;
268                let value_f64 = value.parse::<f64>().unwrap_or_default();
269                evaluate_number_filter(&height_f64, operator, &value_f64)
270            }
271            "context.client.screen-density" => {
272                let density_f64 = self.context.client.screen_density as f64;
273                let value_f64 = value.parse::<f64>().unwrap_or_default();
274                evaluate_number_filter(&density_f64, operator, &value_f64)
275            }
276            "context.client.continent" => {
277                evaluate_string_filter(&self.context.client.continent, operator, value)
278            }
279            "context.client.country-code" => {
280                evaluate_string_filter(&self.context.client.country_code, operator, value)
281            }
282            "context.client.country-name" => {
283                evaluate_string_filter(&self.context.client.country_name, operator, value)
284            }
285            "context.client.region" => {
286                evaluate_string_filter(&self.context.client.region, operator, value)
287            }
288            "context.client.city" => {
289                evaluate_string_filter(&self.context.client.city, operator, value)
290            }
291
292            // Session fields
293            "context.session.session-id" => {
294                evaluate_string_filter(&self.context.session.session_id, operator, value)
295            }
296            "context.session.previous-session-id" => {
297                evaluate_string_filter(&self.context.session.previous_session_id, operator, value)
298            }
299            "context.session.session-count" => {
300                let count_f64 = self.context.session.session_count as f64;
301                let value_f64 = value.parse::<f64>().unwrap_or_default();
302                evaluate_number_filter(&count_f64, operator, &value_f64)
303            }
304            "context.session.session-start" => evaluate_boolean_filter(
305                self.context.session.session_start,
306                operator,
307                value == "true",
308            ),
309            "context.session.first-seen" => {
310                let timestamp_f64 = self.context.session.first_seen.timestamp() as f64;
311                let value_f64 = value.parse::<f64>().unwrap_or_default();
312                evaluate_number_filter(&timestamp_f64, operator, &value_f64)
313            }
314            "context.session.last-seen" => {
315                let timestamp_f64 = self.context.session.last_seen.timestamp() as f64;
316                let value_f64 = value.parse::<f64>().unwrap_or_default();
317                evaluate_number_filter(&timestamp_f64, operator, &value_f64)
318            }
319
320            // Campaign fields
321            "context.campaign.name" => {
322                evaluate_string_filter(&self.context.campaign.name, operator, value)
323            }
324            "context.campaign.source" => {
325                evaluate_string_filter(&self.context.campaign.source, operator, value)
326            }
327            "context.campaign.medium" => {
328                evaluate_string_filter(&self.context.campaign.medium, operator, value)
329            }
330            "context.campaign.term" => {
331                evaluate_string_filter(&self.context.campaign.term, operator, value)
332            }
333            "context.campaign.content" => {
334                evaluate_string_filter(&self.context.campaign.content, operator, value)
335            }
336            "context.campaign.creative-format" => {
337                evaluate_string_filter(&self.context.campaign.creative_format, operator, value)
338            }
339            "context.campaign.marketing-tactic" => {
340                evaluate_string_filter(&self.context.campaign.marketing_tactic, operator, value)
341            }
342
343            // Page data fields
344            "data.page.name" => {
345                if let Data::Page(ref data) = self.data {
346                    evaluate_string_filter(&data.name, operator, value)
347                } else {
348                    false
349                }
350            }
351            "data.page.category" => {
352                if let Data::Page(ref data) = self.data {
353                    evaluate_string_filter(&data.category, operator, value)
354                } else {
355                    false
356                }
357            }
358            "data.page.title" => {
359                if let Data::Page(ref data) = self.data {
360                    evaluate_string_filter(&data.title, operator, value)
361                } else {
362                    false
363                }
364            }
365            "data.page.url" => {
366                if let Data::Page(ref data) = self.data {
367                    evaluate_string_filter(&data.url, operator, value)
368                } else {
369                    false
370                }
371            }
372            "data.page.path" => {
373                if let Data::Page(ref data) = self.data {
374                    evaluate_string_filter(&data.path, operator, value)
375                } else {
376                    false
377                }
378            }
379            "data.page.search" => {
380                if let Data::Page(ref data) = self.data {
381                    evaluate_string_filter(&data.search, operator, value)
382                } else {
383                    false
384                }
385            }
386            "data.page.referrer" => {
387                if let Data::Page(ref data) = self.data {
388                    evaluate_string_filter(&data.referrer, operator, value)
389                } else {
390                    false
391                }
392            }
393
394            // Track data fields
395            "data.track.name" => {
396                if let Data::Track(ref data) = self.data {
397                    evaluate_string_filter(&data.name, operator, value)
398                } else {
399                    false
400                }
401            }
402
403            // User data fields
404            "data.user.user-id" => {
405                if let Data::User(ref data) = self.data {
406                    evaluate_string_filter(&data.user_id, operator, value)
407                } else {
408                    false
409                }
410            }
411            "data.user.anonymous-id" => {
412                if let Data::User(ref data) = self.data {
413                    evaluate_string_filter(&data.anonymous_id, operator, value)
414                } else {
415                    false
416                }
417            }
418            "data.user.edgee-id" => {
419                if let Data::User(ref data) = self.data {
420                    evaluate_string_filter(&data.edgee_id, operator, value)
421                } else {
422                    false
423                }
424            }
425            _ => false,
426        }
427    }
428    pub fn apply_data_manipulation_rules(&mut self, rules: &[ComponentDataManipulationRule]) {
429        rules.iter().for_each(|rule| {
430            rule.event_types
431                .iter()
432                .for_each(|event_type| match event_type.as_str() {
433                    "page" => {
434                        if let Data::Page(ref mut data) = self.data {
435                            rule.manipulations.iter().for_each(|manipulation| {
436                                let value = data.properties.get(&manipulation.from_property);
437                                if let Some(value) = value {
438                                    data.properties
439                                        .insert(manipulation.to_property.clone(), value.clone());
440                                    data.properties.remove(&manipulation.from_property);
441                                }
442                            });
443                        }
444                    }
445                    "track" => {
446                        if let Data::Track(ref mut data) = self.data {
447                            rule.manipulations.iter().for_each(|manipulation| {
448                                if manipulation.manipulation_type == "replace-event-name" {
449                                    if data.name == manipulation.from_property {
450                                        data.name = manipulation.to_property.clone();
451                                    }
452                                } else {
453                                    let value = data.properties.get(&manipulation.from_property);
454                                    if let Some(value) = value {
455                                        data.properties.insert(
456                                            manipulation.to_property.clone(),
457                                            value.clone(),
458                                        );
459                                        data.properties.remove(&manipulation.from_property);
460                                    }
461                                }
462                            });
463                        }
464                    }
465                    "user" => {
466                        if let Data::User(ref mut data) = self.data {
467                            rule.manipulations.iter().for_each(|manipulation| {
468                                let value = data.properties.get(&manipulation.from_property);
469                                if let Some(value) = value {
470                                    data.properties
471                                        .insert(manipulation.to_property.clone(), value.clone());
472                                    data.properties.remove(&manipulation.from_property);
473                                }
474                            });
475                        }
476                    }
477                    _ => {}
478                });
479        });
480    }
481}
482
483pub fn evaluate_boolean_filter(field_value: bool, operator: &str, condition_value: bool) -> bool {
484    match operator {
485        "eq" => field_value == condition_value,
486        "neq" => field_value != condition_value,
487        _ => {
488            error!("Invalid operator: {}", operator);
489            false
490        }
491    }
492}
493
494pub fn evaluate_string_filter(field_value: &str, operator: &str, condition_value: &str) -> bool {
495    match operator {
496        "eq" => field_value == condition_value,
497        "neq" => field_value != condition_value,
498        "in" => condition_value.split(',').any(|v| v.trim() == field_value),
499        "nin" => !condition_value.split(',').any(|v| v.trim() == field_value),
500        "is_null" => field_value.is_empty(),
501        "is_not_null" => !field_value.is_empty(),
502        "sw" => field_value.starts_with(condition_value),
503        "nsw" => !field_value.starts_with(condition_value),
504        _ => {
505            error!("Invalid operator: {}", operator);
506            false
507        }
508    }
509}
510
511pub fn evaluate_number_filter(field_value: &f64, operator: &str, condition_value: &f64) -> bool {
512    match operator {
513        "eq" => field_value == condition_value,
514        "neq" => field_value != condition_value,
515        "gt" => field_value > condition_value,
516        "lt" => field_value < condition_value,
517        "gte" => field_value >= condition_value,
518        "lte" => field_value <= condition_value,
519        _ => {
520            error!("Invalid operator: {}", operator);
521            false
522        }
523    }
524}
525
526#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
527pub enum EventType {
528    #[serde(rename = "page")]
529    #[default]
530    Page,
531    #[serde(rename = "user")]
532    User,
533    #[serde(rename = "track")]
534    Track,
535}
536
537impl fmt::Display for EventType {
538    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
539        match self {
540            EventType::Page => write!(f, "page"),
541            EventType::User => write!(f, "user"),
542            EventType::Track => write!(f, "track"),
543        }
544    }
545}
546
547#[derive(Serialize, Deserialize, Debug, Clone)]
548#[serde(untagged)]
549pub enum Data {
550    Page(Page),
551    User(User),
552    Track(Track),
553}
554
555impl Default for Data {
556    fn default() -> Self {
557        Data::Page(Page::default())
558    }
559}
560
561#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
562pub enum Consent {
563    #[serde(rename = "pending")]
564    Pending,
565    #[serde(rename = "granted")]
566    Granted,
567    #[serde(rename = "denied")]
568    Denied,
569}
570
571impl fmt::Display for Consent {
572    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
573        match self {
574            Consent::Pending => write!(f, "pending"),
575            Consent::Granted => write!(f, "granted"),
576            Consent::Denied => write!(f, "denied"),
577        }
578    }
579}
580
581#[derive(Serialize, Deserialize, Debug, Default, Clone)]
582pub struct Page {
583    // skip serializing the default value
584    #[serde(default, skip_serializing_if = "String::is_empty")]
585    pub name: String,
586    #[serde(default, skip_serializing_if = "String::is_empty")]
587    pub category: String,
588    #[serde(default, skip_serializing_if = "Vec::is_empty")]
589    pub keywords: Vec<String>,
590    pub title: String,
591    pub url: String,
592    pub path: String,
593    #[serde(default, skip_serializing_if = "String::is_empty")]
594    pub search: String,
595    #[serde(default, skip_serializing_if = "String::is_empty")]
596    pub referrer: String,
597    #[serde(default)]
598    pub properties: Dict, // Properties field is free-form
599}
600
601#[derive(Serialize, Deserialize, Debug, Default, Clone)]
602pub struct User {
603    #[serde(default, skip_serializing_if = "String::is_empty")]
604    pub user_id: String,
605    #[serde(default, skip_serializing_if = "String::is_empty")]
606    pub anonymous_id: String,
607    pub edgee_id: String,
608    #[serde(default)]
609    pub properties: Dict, // Properties field is free-form
610    #[serde(skip_serializing)]
611    pub native_cookie_ids: Option<HashMap<String, String>>,
612}
613
614#[derive(Serialize, Deserialize, Debug, Default, Clone)]
615pub struct Track {
616    #[serde(default)]
617    pub name: String,
618    #[serde(default)]
619    pub properties: Dict, // Properties field is free-form
620    #[serde(default, skip_serializing_if = "Vec::is_empty")]
621    pub products: Vec<Dict>,
622}
623
624#[derive(Serialize, Deserialize, Debug, Default, Clone)]
625pub struct Context {
626    #[serde(default)]
627    pub page: Page,
628    pub user: User,
629    pub client: Client,
630    #[serde(default)]
631    pub campaign: Campaign,
632    pub session: Session,
633}
634
635#[allow(dead_code)]
636#[derive(Serialize, Deserialize, Debug, Default, Clone)]
637pub struct Campaign {
638    #[serde(default, skip_serializing_if = "String::is_empty")]
639    pub name: String,
640    #[serde(default, skip_serializing_if = "String::is_empty")]
641    pub source: String,
642    #[serde(default, skip_serializing_if = "String::is_empty")]
643    pub medium: String,
644    #[serde(default, skip_serializing_if = "String::is_empty")]
645    pub term: String,
646    #[serde(default, skip_serializing_if = "String::is_empty")]
647    pub content: String,
648    #[serde(default, skip_serializing_if = "String::is_empty")]
649    pub creative_format: String,
650    #[serde(default, skip_serializing_if = "String::is_empty")]
651    pub marketing_tactic: String,
652}
653
654#[allow(dead_code)]
655#[derive(Serialize, Deserialize, Debug, Default, Clone)]
656pub struct Client {
657    pub ip: String,
658    pub proxy_type: Option<String>,
659    pub proxy_desc: Option<String>,
660    pub as_name: Option<String>,
661    pub as_number: Option<u32>,
662
663    #[serde(default, skip_serializing_if = "String::is_empty")]
664    pub locale: String,
665    #[serde(default, skip_serializing_if = "String::is_empty")]
666    pub accept_language: String,
667    #[serde(default, skip_serializing_if = "String::is_empty")]
668    pub timezone: String,
669    pub user_agent: String,
670
671    // Low Entropy Client Hint Data - from sec-ch-ua header
672    // The brand and version information for each brand associated with the browser, in a comma-separated list. ex: "Chromium;130|Google Chrome;130|Not?A_Brand;99"
673    #[serde(default, skip_serializing_if = "String::is_empty")]
674    pub user_agent_version_list: String,
675
676    // Low Entropy Client Hint Data - from Sec-Ch-Ua-Mobile header
677    // Indicates whether the browser is on a mobile device. ex: 0
678    #[serde(default, skip_serializing_if = "String::is_empty")]
679    pub user_agent_mobile: String,
680
681    // Low Entropy Client Hint Data - from Sec-Ch-Ua-Platform header
682    // The platform or operating system on which the user agent is running. Ex: macOS
683    #[serde(default, skip_serializing_if = "String::is_empty")]
684    pub os_name: String,
685
686    // High Entropy Client Hint Data - from Sec-Ch-Ua-Arch header
687    // User Agent Architecture. ex: arm
688    #[serde(default, skip_serializing_if = "String::is_empty")]
689    pub user_agent_architecture: String,
690
691    // High Entropy Client Hint Data - from Sec-Ch-Ua-Bitness header
692    // The "bitness" of the user-agent's underlying CPU architecture. This is the size in bits of an integer or memory address—typically 64 or 32 bits. ex: 64
693    #[serde(default, skip_serializing_if = "String::is_empty")]
694    pub user_agent_bitness: String,
695
696    // High Entropy Client Hint Data - from Sec-Ch-Ua-Full-Version-List header
697    // The brand and full version information for each brand associated with the browser, in a comma-separated list. ex: Chromium;112.0.5615.49|Google Chrome;112.0.5615.49|Not?A-Brand;99.0.0.0
698    #[serde(default, skip_serializing_if = "String::is_empty")]
699    pub user_agent_full_version_list: String,
700
701    // High Entropy Client Hint Data - from Sec-Ch-Ua-Model header
702    // The device model on which the browser is running. Will likely be empty for desktop browsers. ex: Nexus 6
703    #[serde(default, skip_serializing_if = "String::is_empty")]
704    pub user_agent_model: String,
705
706    // High Entropy Client Hint Data - from Sec-Ch-Ua-Platform-Version header
707    // The version of the operating system on which the user agent is running. Ex: 12.2.1
708    #[serde(default, skip_serializing_if = "String::is_empty")]
709    pub os_version: String,
710
711    #[serde(default)]
712    pub screen_width: i32,
713
714    #[serde(default)]
715    pub screen_height: i32,
716
717    #[serde(default)]
718    pub screen_density: f32,
719
720    #[serde(default, skip_serializing_if = "String::is_empty")]
721    pub continent: String,
722
723    #[serde(default, skip_serializing_if = "String::is_empty")]
724    pub country_code: String,
725
726    #[serde(default, skip_serializing_if = "String::is_empty")]
727    pub country_name: String,
728
729    #[serde(default, skip_serializing_if = "String::is_empty")]
730    pub region: String,
731
732    #[serde(default, skip_serializing_if = "String::is_empty")]
733    pub city: String,
734}
735
736#[derive(Serialize, Deserialize, Debug, Clone, Default)]
737pub struct Session {
738    pub session_id: String,
739    #[serde(default)]
740    pub previous_session_id: String,
741    pub session_count: u32,
742    pub session_start: bool,
743    pub first_seen: DateTime<Utc>,
744    pub last_seen: DateTime<Utc>,
745}