edgee_components_runtime/data_collection/
mod.rs

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    // iterate on each event
206    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            // if event_type is not enabled in config.config.get(component_id).unwrap(), skip the event
237            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            // Use the helper function to handle consent and determine anonymization
266            let (anonymization, outgoing_consent) = handle_consent_and_anonymization(
267                &mut event,
268                &default_consent,
269                initial_anonymization,
270            );
271
272            if anonymization {
273                // anonymize the ip
274                event.context.client.ip = ctx.get_ip_anonymized().clone();
275
276                // TODO: optionally remove query params from the url
277                // event.context.page.url = event
278                //     .context
279                //     .page
280                //     .url
281                //     .split('?')
282                //     .next()
283                //     .unwrap_or(&event.context.page.url)
284                //     .to_string();
285
286                // remove search
287                event.context.page.search = "".to_string();
288
289                // remove referrer
290                event.context.page.referrer = "".to_string();
291
292                // remove campaign data
293                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                // TODO: optionally remove query params from the url
302                // if event.event_type is page, remove the page_search and page_referrer
303                // if event.event_type == EventType::Page {
304                //     if let Data::Page(page) = &mut event.data {
305                //         // remove query params from the url
306                //         page.url = page.url.split('?').next().unwrap_or(&page.url).to_string();
307                //         page.search = "".to_string();
308                //         page.referrer = "".to_string();
309                //     }
310                // }
311            } else {
312                event.context.client.ip = ctx.get_ip().clone();
313            }
314
315            // Native cookie support
316            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            // Add one second to the timestamp if uuid is not the same than the first event, to prevent duplicate sessions
325            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            // spawn a separated async thread
382            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    // Handle default consent if not set
530    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    // Determine final anonymization state
541    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    // Insert client ip in the x-forwarded-for header
553    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    // Insert User-Agent in the user-agent header
561    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    // Insert referrer in the referer header like an analytics client-side collect does
569    // (but without query string to avoid privacy issues)
570    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    // Insert Accept-Language in the accept-language header
575    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    // Insert origin in the origin header
583    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    // Insert Accept header
590    headers.insert(header::ACCEPT, HeaderValue::from_str("*/*")?);
591
592    // Insert sec-fetch-dest, sec-fetch-mode and sec-fetch-site headers
593    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    // Insert headers from the real client headers
607    for (key, value) in client_headers.iter() {
608        headers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?);
609    }
610
611    Ok(())
612}
613
614/// Extracts the origin from a URL string.
615///
616/// The origin consists of the scheme and host components of the URL.
617/// For example, given "https://www.example.com/test", this function returns "https://www.example.com".
618///
619/// # Arguments
620///
621/// * `url` - A string slice containing the URL to parse
622///
623/// # Returns
624///
625/// * `Result<String, anyhow::Error>` - The origin string on success, or an error if URL parsing fails
626///
627pub 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
634/// Extracts the URL without query string from a URL string.
635///
636/// For example, given "https://www.example.com/test?query=test", this function returns "https://www.example.com/test".
637///
638/// # Arguments
639///
640/// * `url` - A string slice containing the URL to parse
641///
642/// # Returns
643///
644/// * `Result<String, anyhow::Error>` - The URL string without query string on success, or an error if URL parsing fails
645///
646pub 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        // Valid cases
663        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        // Edge cases
693        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        // Call the function
801        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        // Test with default consent "granted"
815        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        // Test with default consent "denied"
825        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        // Test with default consent "pending"
839        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        // Test with existing consent "granted"
853        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        // Test with existing consent "denied"
867        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        // Test with existing consent "pending"
881        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        // verify the future's result
1007        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}