1mod context;
2mod convert;
3mod debug;
4pub mod logger;
5pub mod payload;
6pub mod versions;
7
8use std::str::FromStr;
9use std::time::Duration;
10use url::Url;
11
12use crate::config::{ComponentsConfiguration, DataCollectionComponents};
13use context::EventContext;
14use debug::{debug_and_trace_response, trace_disabled_event, trace_request, DebugParams};
15use http::{header, HeaderMap, HeaderName, HeaderValue};
16use tokio::task::JoinHandle;
17use tracing::{error, span, Instrument, Level};
18
19use crate::context::ComponentsContext;
20
21use crate::data_collection::payload::{Consent, Event, EventType};
22use std::collections::HashMap;
23
24#[derive(Clone)]
25pub struct ComponentMetadata {
26 pub component_id: String,
27 pub component: String,
28 pub anonymization: bool,
29}
30
31#[derive(Clone)]
32pub struct Response {
33 pub status: i32,
34 pub body: String,
35 pub content_type: String,
36 pub message: String,
37 pub duration: u128,
38}
39
40#[derive(Clone)]
41pub struct Request {
42 pub method: String,
43 pub url: String,
44 pub body: String,
45 pub headers: HashMap<String, String>,
46}
47
48#[derive(Clone)]
49pub struct EventResponse {
50 pub context: EventContext,
51 pub event: Event,
52 pub component_metadata: ComponentMetadata,
53
54 pub response: Response,
55 pub request: Request,
56}
57
58#[derive(Clone)]
59pub struct AuthResponse {
60 pub component_id: String,
61 pub serialized_token_content: String,
62 pub token_duration: i64,
63 pub component_token_setting_name: String,
64}
65
66pub async fn send_json_events(
67 component_ctx: &ComponentsContext,
68 events_json: &str,
69 component_config: &ComponentsConfiguration,
70 trace_component: &Option<String>,
71 debug: bool,
72) -> anyhow::Result<Vec<JoinHandle<EventResponse>>> {
73 if events_json.is_empty() {
74 return Ok(vec![]);
75 }
76
77 let mut events: Vec<Event> = serde_json::from_str(events_json)?;
78 send_events(
79 component_ctx,
80 &mut events,
81 component_config,
82 trace_component,
83 debug,
84 "",
85 "",
86 &HashMap::new(),
87 )
88 .await
89}
90
91pub async fn get_auth_request(
92 context: &ComponentsContext,
93 component: &DataCollectionComponents,
94) -> anyhow::Result<Option<JoinHandle<AuthResponse>>> {
95 let mut store = context.empty_store();
96 let (headers, method, url, body, auth_metadata) =
97 match crate::data_collection::versions::v1_0_1::execute::get_auth_request(
98 context, component, &mut store,
99 )
100 .await
101 {
102 Ok(Some((headers, method, url, body, auth_metadata))) => {
103 (headers, method, url, body, auth_metadata)
104 }
105 Ok(None) => {
106 return Ok(None);
107 }
108 Err(err) => {
109 error!("Failed to get auth request. Error: {}", err);
110 return Err(err);
111 }
112 };
113
114 let client = reqwest::Client::builder()
115 .timeout(Duration::from_secs(5))
116 .build()?;
117
118 let component_id = component.id.clone();
119 let future = tokio::spawn(async move {
120 let res = match method.as_str() {
121 "GET" => client.get(url.clone()).headers(headers).send().await,
122 "PUT" => {
123 client
124 .put(url.clone())
125 .headers(headers)
126 .body(body.clone())
127 .send()
128 .await
129 }
130 "POST" => {
131 client
132 .post(url.clone())
133 .headers(headers)
134 .body(body.clone())
135 .send()
136 .await
137 }
138 _ => {
139 return AuthResponse {
140 component_id,
141 serialized_token_content: String::new(),
142 component_token_setting_name: auth_metadata.component_token_setting_name,
143 token_duration: auth_metadata.token_duration,
144 }
145 }
146 };
147
148 let res = res.unwrap();
149 let response_text = res.text().await.unwrap_or_default();
150 let json_value = serde_json::from_str::<serde_json::Value>(&response_text).ok();
151
152 let expires_in = json_value
153 .as_ref()
154 .and_then(|json| json.get("expires_in").and_then(|v| v.as_i64()));
155
156 let serialized_token_content = json_value
157 .and_then(|json| {
158 if auth_metadata.response_token_property_name.is_none() {
159 Some(response_text)
160 } else {
161 auth_metadata
162 .response_token_property_name
163 .as_ref()
164 .and_then(|property_name| json.get(property_name))
165 .and_then(|v| v.as_str().map(|s| s.to_string()))
166 }
167 })
168 .unwrap_or_else(|| {
169 error!("Failed to find token property");
170 String::new()
171 });
172
173 AuthResponse {
174 component_id,
175 serialized_token_content,
176 component_token_setting_name: auth_metadata.component_token_setting_name,
177 token_duration: expires_in.unwrap_or(auth_metadata.token_duration),
178 }
179 });
180
181 Ok(Some(future))
182}
183
184#[allow(clippy::too_many_arguments)]
185pub async fn send_events(
186 component_ctx: &ComponentsContext,
187 events: &mut [Event],
188 component_config: &ComponentsConfiguration,
189 trace_component: &Option<String>,
190 debug: bool,
191 project_id: &str,
192 proxy_host: &str,
193 client_headers: &HashMap<String, String>,
194) -> anyhow::Result<Vec<JoinHandle<EventResponse>>> {
195 if events.is_empty() {
196 return Ok(vec![]);
197 }
198
199 let ctx = &EventContext::new(events, project_id, proxy_host);
200
201 let mut store = component_ctx.empty_store();
202
203 let mut futures = vec![];
204
205 for event in events.iter_mut() {
207 for cfg in component_config.data_collection.iter() {
208 let span = span!(
209 Level::INFO,
210 "component",
211 name = cfg.id.as_str(),
212 event = ?event.event_type
213 );
214 let _enter = span.enter();
215
216 let mut event = event.clone();
217
218 let trace =
219 trace_component.is_some() && trace_component.as_ref().unwrap() == cfg.id.as_str();
220
221 if cfg.event_filtering_rules.iter().any(|rule| {
222 rule.event_types
223 .iter()
224 .any(|event_type| event_type == &event.event_type.to_string())
225 && rule
226 .conditions
227 .iter()
228 .any(|condition| event.should_filter_out(condition))
229 }) {
230 trace_disabled_event(trace, "event_filtered");
231 continue;
232 }
233
234 event.apply_data_manipulation_rules(&cfg.data_manipulation_rules);
235
236 match event.event_type {
238 EventType::Page => {
239 if !cfg.settings.edgee_page_event_enabled {
240 trace_disabled_event(trace, "page");
241 continue;
242 }
243 }
244 EventType::User => {
245 if !cfg.settings.edgee_user_event_enabled {
246 trace_disabled_event(trace, "user");
247 continue;
248 }
249 }
250 EventType::Track => {
251 if !cfg.settings.edgee_track_event_enabled {
252 trace_disabled_event(trace, "track");
253 continue;
254 }
255 }
256 }
257
258 if !event.is_component_enabled(cfg) {
259 continue;
260 }
261
262 let initial_anonymization = cfg.settings.edgee_anonymization;
263 let default_consent = cfg.settings.edgee_default_consent.clone();
264
265 let (anonymization, outgoing_consent) = handle_consent_and_anonymization(
267 &mut event,
268 &default_consent,
269 initial_anonymization,
270 );
271
272 if anonymization {
273 event.context.client.ip = ctx.get_ip_anonymized().clone();
275
276 event.context.page.search = "".to_string();
288
289 event.context.page.referrer = "".to_string();
291
292 event.context.campaign.medium = "".to_string();
294 event.context.campaign.name = "".to_string();
295 event.context.campaign.source = "".to_string();
296 event.context.campaign.content = "".to_string();
297 event.context.campaign.creative_format = "".to_string();
298 event.context.campaign.marketing_tactic = "".to_string();
299 event.context.campaign.term = "".to_string();
300
301 } else {
312 event.context.client.ip = ctx.get_ip().clone();
313 }
314
315 if let Some(ref ids) = event.context.user.native_cookie_ids {
317 if ids.contains_key(&cfg.slug) {
318 event.context.user.edgee_id = ids.get(&cfg.slug).unwrap().clone();
319 } else {
320 event.context.user.edgee_id = ctx.get_edgee_id().clone();
321 }
322 }
323
324 if &event.uuid != ctx.get_uuid() {
326 event.timestamp = *ctx.get_timestamp() + chrono::Duration::seconds(1);
327 event.context.session.session_start = false;
328 }
329
330 let (headers, method, url, body) = match cfg.wit_version {
331 versions::DataCollectionWitVersion::V1_0_0 => {
332 match crate::data_collection::versions::v1_0_0::execute::get_edgee_request(
333 &event,
334 component_ctx,
335 cfg,
336 &mut store,
337 &client_headers.clone(),
338 )
339 .await
340 {
341 Ok((headers, method, url, body)) => (headers, method, url, body),
342 Err(err) => {
343 error!("Failed to get edgee request. Error: {}", err);
344 continue;
345 }
346 }
347 }
348 versions::DataCollectionWitVersion::V1_0_1 => {
349 match crate::data_collection::versions::v1_0_1::execute::get_edgee_request(
350 &event,
351 component_ctx,
352 cfg,
353 &mut store,
354 &client_headers.clone(),
355 )
356 .await
357 {
358 Ok((headers, method, url, body)) => (headers, method, url, body),
359 Err(err) => {
360 error!("Failed to get edgee request. Error: {}", err);
361 continue;
362 }
363 }
364 }
365 };
366
367 let client = reqwest::Client::builder()
368 .timeout(Duration::from_secs(5))
369 .build()?;
370
371 trace_request(
372 trace,
373 &method,
374 &url,
375 &headers,
376 &body,
377 &outgoing_consent,
378 anonymization,
379 );
380
381 let cfg_project_component_id = cfg.project_component_id.to_string();
383 let cfg_id = cfg.id.to_string();
384 let ctx_clone = ctx.clone();
385
386 let headers_map = headers.iter().fold(HashMap::new(), |mut acc, (k, v)| {
387 acc.insert(k.to_string(), v.to_str().unwrap().to_string());
388 acc
389 });
390
391 let method_clone = method.to_string();
392 let url_clone = url.clone();
393 let body_clone = body.clone();
394
395 let future = tokio::spawn(
396 async move {
397 let timer = std::time::Instant::now();
398 let res = match method_clone.as_str() {
399 "GET" => client.get(url_clone).headers(headers).send().await,
400 "PUT" => {
401 client
402 .put(url_clone)
403 .headers(headers)
404 .body(body_clone)
405 .send()
406 .await
407 }
408 "POST" => {
409 client
410 .post(url_clone)
411 .headers(headers)
412 .body(body_clone)
413 .send()
414 .await
415 }
416 "DELETE" => client.delete(url_clone).headers(headers).send().await,
417 _ => {
418 return EventResponse {
419 context: ctx_clone,
420 event,
421 component_metadata: ComponentMetadata {
422 component_id: cfg_project_component_id,
423 component: cfg_id,
424 anonymization,
425 },
426 response: Response {
427 status: 500,
428 body: "".to_string(),
429 content_type: "text/plain".to_string(),
430 message: "Unknown method".to_string(),
431 duration: timer.elapsed().as_millis(),
432 },
433 request: Request {
434 method: method_clone,
435 url: url_clone,
436 body: body_clone,
437 headers: headers_map,
438 },
439 }
440 }
441 };
442
443 let mut debug_params = DebugParams::new(
444 &ctx_clone,
445 &cfg_project_component_id,
446 &cfg_id,
447 &event,
448 &method_clone,
449 &url,
450 &headers_map,
451 &body,
452 timer,
453 anonymization,
454 );
455
456 let mut message = "".to_string();
457 match res {
458 Ok(res) => {
459 debug_params.response_status =
460 format!("{:?}", res.status()).parse::<i32>().unwrap();
461
462 debug_params.response_content_type = res
463 .headers()
464 .get("content-type")
465 .and_then(|v| v.to_str().ok())
466 .unwrap_or("text/plain")
467 .to_string();
468
469 debug_params.response_body = Some(res.text().await.unwrap_or_default());
470
471 let _r = debug_and_trace_response(
472 debug,
473 trace,
474 debug_params.clone(),
475 "".to_string(),
476 )
477 .await;
478 }
479 Err(err) => {
480 error!(step = "response", status = "500", err = err.to_string());
481 let _r = debug_and_trace_response(
482 debug,
483 trace,
484 debug_params.clone(),
485 err.to_string(),
486 )
487 .await;
488 message = err.to_string();
489 }
490 }
491
492 EventResponse {
493 context: ctx_clone,
494 event,
495 component_metadata: ComponentMetadata {
496 component_id: cfg_project_component_id,
497 component: cfg_id,
498 anonymization,
499 },
500 response: Response {
501 status: debug_params.response_status,
502 body: debug_params.response_body.unwrap_or_default(),
503 content_type: debug_params.response_content_type,
504 message,
505 duration: timer.elapsed().as_millis(),
506 },
507 request: Request {
508 method,
509 url: url.to_string(),
510 body,
511 headers: headers_map,
512 },
513 }
514 }
515 .in_current_span(),
516 );
517 futures.push(future);
518 }
519 }
520
521 Ok(futures)
522}
523
524fn handle_consent_and_anonymization(
525 event: &mut Event,
526 default_consent: &str,
527 initial_anonymization: bool,
528) -> (bool, String) {
529 if event.consent.is_none() {
531 event.consent = match default_consent {
532 "granted" => Some(Consent::Granted),
533 "denied" => Some(Consent::Denied),
534 _ => Some(Consent::Pending),
535 };
536 }
537
538 let outgoing_consent = event.consent.clone().unwrap().to_string();
539
540 match event.consent {
542 Some(Consent::Granted) => (false, outgoing_consent),
543 _ => (initial_anonymization, outgoing_consent),
544 }
545}
546
547pub fn insert_expected_headers(
548 headers: &mut HeaderMap,
549 event: &Event,
550 client_headers: &HashMap<String, String>,
551) -> anyhow::Result<()> {
552 if !event.context.client.ip.is_empty() {
554 headers.insert(
555 HeaderName::from_str("x-forwarded-for")?,
556 HeaderValue::from_str(&event.context.client.ip)?,
557 );
558 }
559
560 if !event.context.client.user_agent.is_empty() {
562 headers.insert(
563 header::USER_AGENT,
564 HeaderValue::from_str(&event.context.client.user_agent)?,
565 );
566 }
567
568 if let Ok(url) = get_url_without_query_string(&event.context.page.url) {
571 headers.insert(header::REFERER, HeaderValue::from_str(url.as_str())?);
572 }
573
574 if !event.context.client.accept_language.is_empty() {
576 headers.insert(
577 header::ACCEPT_LANGUAGE,
578 HeaderValue::from_str(event.context.client.accept_language.as_str())?,
579 );
580 }
581
582 if let Ok(origin) = get_origin_from_url(&event.context.page.url) {
584 if let Ok(value) = HeaderValue::from_str(&origin) {
585 headers.insert(header::ORIGIN, value);
586 }
587 }
588
589 headers.insert(header::ACCEPT, HeaderValue::from_str("*/*")?);
591
592 headers.insert(
594 HeaderName::from_str("sec-fetch-dest")?,
595 HeaderValue::from_str("empty")?,
596 );
597 headers.insert(
598 HeaderName::from_str("sec-fetch-mode")?,
599 HeaderValue::from_str("no-cors")?,
600 );
601 headers.insert(
602 HeaderName::from_str("sec-fetch-site")?,
603 HeaderValue::from_str("cross-site")?,
604 );
605
606 for (key, value) in client_headers.iter() {
608 headers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?);
609 }
610
611 Ok(())
612}
613
614pub fn get_origin_from_url(url: &str) -> Result<String, anyhow::Error> {
628 let url = Url::parse(url)?;
629 let host = url.host_str().unwrap_or_default();
630 let scheme = url.scheme();
631 Ok(format!("{scheme}://{host}"))
632}
633
634pub fn get_url_without_query_string(url: &str) -> Result<String, anyhow::Error> {
647 let url = Url::parse(url)?;
648 let host = url.host_str().unwrap_or_default();
649 let scheme = url.scheme();
650 let path = url.path();
651 Ok(format!("{scheme}://{host}{path}"))
652}
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::config::{DataCollectionComponentSettings, DataCollectionComponents};
657 use crate::data_collection::payload::{Client, Context, Page};
658 use http::HeaderValue;
659
660 #[test]
661 fn test_get_origin_from_url() {
662 assert_eq!(
664 get_origin_from_url("https://www.example.com/test").unwrap(),
665 "https://www.example.com"
666 );
667 assert_eq!(
668 get_origin_from_url("https://de.example.com").unwrap(),
669 "https://de.example.com"
670 );
671 assert_eq!(
672 get_origin_from_url("https://www.example.com/test?utm_source=test").unwrap(),
673 "https://www.example.com"
674 );
675 assert_eq!(
676 get_origin_from_url("https://www.example.com:8080").unwrap(),
677 "https://www.example.com"
678 );
679 assert_eq!(
680 get_origin_from_url("https://www.example.com:8080/test?query=test").unwrap(),
681 "https://www.example.com"
682 );
683 assert_eq!(
684 get_origin_from_url("https://www.example.com:8080?query=test").unwrap(),
685 "https://www.example.com"
686 );
687 assert_eq!(
688 get_origin_from_url("https://www.example.com:8080/test?query=test").unwrap(),
689 "https://www.example.com"
690 );
691
692 assert!(get_origin_from_url("").is_err());
694 assert!(get_origin_from_url("Invalid").is_err());
695 assert!(get_origin_from_url("No Version;").is_err());
696 assert!(get_origin_from_url(";No Brand").is_err());
697 }
698
699 fn create_test_event() -> Event {
700 Event {
701 context: Context {
702 client: Client {
703 ip: "192.168.1.1".to_string(),
704 user_agent: "Mozilla/5.0".to_string(),
705 accept_language: "en-US,en;q=0.9".to_string(),
706 user_agent_version_list: "Chromium;128|Google Chrome;128".to_string(),
707 user_agent_mobile: "0".to_string(),
708 os_name: "Windows".to_string(),
709 ..Default::default()
710 },
711 page: Page {
712 url: "https://example.com".to_string(),
713 search: "?query=test".to_string(),
714 ..Default::default()
715 },
716 ..Default::default()
717 },
718 ..Default::default()
719 }
720 }
721
722 fn create_empty_test_event() -> Event {
723 Event {
724 context: Context {
725 client: Client {
726 ip: "".to_string(),
727 user_agent: "".to_string(),
728 accept_language: "".to_string(),
729 user_agent_version_list: "".to_string(),
730 user_agent_mobile: "".to_string(),
731 os_name: "".to_string(),
732 ..Default::default()
733 },
734 page: Page {
735 url: "".to_string(),
736 search: "".to_string(),
737 ..Default::default()
738 },
739 ..Default::default()
740 },
741 ..Default::default()
742 }
743 }
744
745 #[test]
746 fn test_insert_expected_headers() {
747 let mut headers = HeaderMap::new();
748 let event = create_test_event();
749
750 let mut client_headers: HashMap<String, String> = HashMap::new();
751 client_headers.insert(
752 "sec-ch-ua".to_string(),
753 "\"Chromium\";v=\"128\", \"Google Chrome\";v=\"128\"".to_string(),
754 );
755 client_headers.insert("sec-ch-ua-mobile".to_string(), "?0".to_string());
756 client_headers.insert("sec-ch-ua-platform".to_string(), "\"Windows\"".to_string());
757
758 let result = insert_expected_headers(&mut headers, &event, &client_headers);
759
760 assert!(result.is_ok());
761 assert_eq!(
762 headers.get("x-forwarded-for"),
763 Some(&HeaderValue::from_str("192.168.1.1").unwrap())
764 );
765 assert_eq!(
766 headers.get("user-agent"),
767 Some(&HeaderValue::from_str("Mozilla/5.0").unwrap())
768 );
769 assert_eq!(
770 headers.get("accept-language"),
771 Some(&HeaderValue::from_str("en-US,en;q=0.9").unwrap())
772 );
773 assert_eq!(
774 headers.get("referer"),
775 Some(&HeaderValue::from_str("https://example.com/").unwrap())
776 );
777 assert_eq!(
778 headers.get("sec-ch-ua"),
779 Some(
780 &HeaderValue::from_str("\"Chromium\";v=\"128\", \"Google Chrome\";v=\"128\"")
781 .unwrap()
782 )
783 );
784 assert_eq!(
785 headers.get("sec-ch-ua-mobile"),
786 Some(&HeaderValue::from_str("?0").unwrap())
787 );
788 assert_eq!(
789 headers.get("sec-ch-ua-platform"),
790 Some(&HeaderValue::from_str("\"Windows\"").unwrap())
791 );
792 }
793
794 #[test]
795 fn test_insert_expected_headers_with_empty_fields() {
796 let mut headers = HeaderMap::new();
797
798 let event = create_empty_test_event();
799
800 let result = insert_expected_headers(&mut headers, &event, &HashMap::new());
802
803 assert!(result.is_ok());
804 assert_eq!(headers.keys().len(), 4);
805 }
806
807 #[test]
808 fn test_handle_consent_and_anonymization_granted() {
809 let mut event = Event {
810 consent: None,
811 ..Default::default()
812 };
813
814 let (anonymization, outgoing_consent) =
816 handle_consent_and_anonymization(&mut event, "granted", true);
817 assert_eq!(event.consent, Some(Consent::Granted));
818 assert!(!anonymization);
819 assert_eq!(outgoing_consent, "granted");
820 }
821
822 #[test]
823 fn test_handle_consent_and_anonymization_denied() {
824 let mut event = Event {
826 consent: None,
827 ..Default::default()
828 };
829 let (anonymization, outgoing_consent) =
830 handle_consent_and_anonymization(&mut event, "denied", true);
831 assert_eq!(event.consent, Some(Consent::Denied));
832 assert!(anonymization);
833 assert_eq!(outgoing_consent, "denied");
834 }
835
836 #[test]
837 fn test_handle_consent_and_anonymization_pending() {
838 let mut event = Event {
840 consent: None,
841 ..Default::default()
842 };
843 let (anonymization, outgoing_consent) =
844 handle_consent_and_anonymization(&mut event, "pending", true);
845 assert_eq!(event.consent, Some(Consent::Pending));
846 assert!(anonymization);
847 assert_eq!(outgoing_consent, "pending");
848 }
849
850 #[test]
851 fn test_handle_consent_and_anonymization_existing_granted() {
852 let mut event = Event {
854 consent: Some(Consent::Granted),
855 ..Default::default()
856 };
857 let (anonymization, outgoing_consent) =
858 handle_consent_and_anonymization(&mut event, "denied", true);
859 assert_eq!(event.consent, Some(Consent::Granted));
860 assert!(!anonymization);
861 assert_eq!(outgoing_consent, "granted");
862 }
863
864 #[test]
865 fn test_handle_consent_and_anonymization_existing_denied() {
866 let mut event = Event {
868 consent: Some(Consent::Denied),
869 ..Default::default()
870 };
871 let (anonymization, outgoing_consent) =
872 handle_consent_and_anonymization(&mut event, "granted", false);
873 assert_eq!(event.consent, Some(Consent::Denied));
874 assert!(!anonymization);
875 assert_eq!(outgoing_consent, "denied");
876 }
877
878 #[test]
879 fn test_handle_consent_and_anonymization_existing_pending() {
880 let mut event = Event {
882 consent: Some(Consent::Pending),
883 ..Default::default()
884 };
885 let (anonymization, outgoing_consent) =
886 handle_consent_and_anonymization(&mut event, "granted", true);
887 assert_eq!(event.consent, Some(Consent::Pending));
888 assert!(anonymization);
889 assert_eq!(outgoing_consent, "pending");
890 }
891
892 #[tokio::test]
893 async fn test_send_json_events_with_empty_json() {
894 let component_config = ComponentsConfiguration::default();
895 let component_ctx = ComponentsContext::new(&component_config).unwrap();
896 let events_json = "";
897 let trace_component = None;
898 let debug = false;
899
900 let result = send_json_events(
901 &component_ctx,
902 events_json,
903 &component_config,
904 &trace_component,
905 debug,
906 )
907 .await;
908
909 assert!(result.is_ok());
910 assert!(result.unwrap().is_empty());
911 }
912
913 fn create_sample_json_events() -> String {
914 r#"[{
915 "context": {
916 "client": {
917 "ip": "192.168.1.1",
918 "user_agent": "Mozilla/5.0",
919 "accept_language": "en-US,en;q=0.9",
920 "user_agent_version_list": "Chromium;128|Google Chrome;128",
921 "user_agent_mobile": "0",
922 "os_name": "Windows"
923 },
924 "session": {
925 "session_id": "12345",
926 "previous_session_id": "67890",
927 "session_start": true,
928 "session_count": 123,
929 "first_seen": "2023-01-01T00:00:00Z",
930 "last_seen": "2023-01-01T00:00:00Z"
931 },
932 "page": {
933 "title": "Test Page",
934 "referrer": "https://example.com",
935 "path": "/test",
936 "url": "https://example.com/test",
937 "search": "?query=test"
938 },
939 "user": {
940 "edgee_id": "abc123"
941 }
942 },
943 "data": {
944 "title": "Test Page",
945 "referrer": "https://example.com",
946 "path": "/test",
947 "url": "https://example.com/test",
948 "search": "?query=test"
949 },
950 "type": "page",
951 "uuid": "12345",
952 "timestamp": "2025-01-01T00:00:00Z",
953 "consent": "granted"
954 }]"#
955 .to_string()
956 }
957
958 fn create_component_config() -> ComponentsConfiguration {
959 let mut component_config = ComponentsConfiguration::default();
960 component_config
961 .data_collection
962 .push(DataCollectionComponents {
963 id: "test_component".to_string(),
964 slug: "test_slug".to_string(),
965 file: String::from("tests/ga.wasm"),
966 project_component_id: "test_project_component_id".to_string(),
967 settings: DataCollectionComponentSettings {
968 edgee_page_event_enabled: true,
969 edgee_user_event_enabled: true,
970 edgee_track_event_enabled: true,
971 edgee_anonymization: true,
972 edgee_default_consent: "granted".to_string(),
973 additional_settings: {
974 let mut map = HashMap::new();
975 map.insert("ga_measurement_id".to_string(), "abcdefg".to_string());
976 map
977 },
978 },
979 wit_version: versions::DataCollectionWitVersion::V1_0_0,
980 event_filtering_rules: vec![],
981 data_manipulation_rules: vec![],
982 });
983 component_config
984 }
985
986 #[tokio::test]
987 async fn test_send_json_events_with_single_event() {
988 let component_config = create_component_config();
989 let ctx = ComponentsContext::new(&component_config).unwrap();
990
991 let result = send_json_events(
992 &ctx,
993 create_sample_json_events().as_str(),
994 &component_config,
995 &None,
996 false,
997 )
998 .await;
999
1000 let handles = result.unwrap_or_else(|err| {
1001 println!("Error: {:?}", err);
1002 panic!("Test failed");
1003 });
1004 assert_eq!(handles.len(), 1);
1005
1006 let event_response = handles.into_iter().next().unwrap().await.unwrap();
1008 assert_eq!(event_response.event.event_type, EventType::Page);
1009 assert_eq!(event_response.event.context.client.ip, "192.168.1.1");
1010 assert_eq!(
1011 event_response.event.context.page.url,
1012 "https://example.com/test"
1013 );
1014 assert_eq!(event_response.event.consent, Some(Consent::Granted));
1015 }
1016}