edgee-components-runtime 1.2.27

Edgee components runtime (using wasmtime)
Documentation
use crate::data_collection::payload::{Consent, Context, Data, Event, EventType};
use chrono::{DateTime, Utc};
use http::HeaderMap;
use json_pretty::PrettyFormatter;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::time::Duration;

use super::{context::EventContext, logger::Logger};

#[derive(Serialize, Debug, Clone, Default)]
pub struct DebugPayload {
    pub uuid: String,
    pub timestamp: DateTime<Utc>,
    #[serde(rename = "type")]
    pub event_type: String,
    pub from: String,
    pub data: Data,
    pub context: Context,
    pub incoming_consent: String,
    pub outgoing_consent: String,
    pub anonymization: String,
    pub project_id: String,
    pub component_id: String,
    pub component_slug: String,
    pub component_request: DebugComponentRequest,
    pub component_response: DebugComponentResponse,
}

impl DebugPayload {
    pub fn new(params: DebugParams, error: &str) -> Self {
        let component_request = DebugComponentRequest::new(
            &params.method.to_string(),
            &params.url,
            &params.headers,
            &params.body,
        );

        let mut body = params.response_body;
        if !error.is_empty() {
            body = Some(error.to_string())
        }
        let component_response = DebugComponentResponse::new(
            params.response_status,
            body,
            params.response_content_type.to_string(),
            params.timer.elapsed().as_millis() as i32,
        );

        Self {
            uuid: params.event.uuid.clone(),
            timestamp: params.event.timestamp,
            event_type: match params.event.event_type {
                EventType::Page => "page".to_string(),
                EventType::User => "user".to_string(),
                EventType::Track => "track".to_string(),
            },
            from: params.from.to_string(),
            project_id: params.project_id.to_string(),
            data: params.event.data.clone(),
            context: params.event.context.clone(),
            incoming_consent: params.incoming_consent.to_string(),
            outgoing_consent: match params.event.consent.clone().unwrap() {
                Consent::Granted => "granted".to_string(),
                Consent::Denied => "denied".to_string(),
                Consent::Pending => "pending".to_string(),
            },
            anonymization: params.anonymization.to_string(),
            component_id: params.component_id.to_string(),
            component_slug: params.component_slug.to_string(),
            component_request,
            component_response,
        }
    }
}

#[derive(Clone)]
pub struct DebugParams {
    pub from: String,
    pub project_id: String,
    pub component_id: String,
    pub component_slug: String,
    pub event: Event,
    pub method: String,
    pub url: String,
    pub headers: HashMap<String, String>,
    pub body: String,
    pub response_content_type: String,
    pub response_status: i32,
    pub response_body: Option<String>,
    pub timer: std::time::Instant,
    pub anonymization: bool,
    pub incoming_consent: String,
    pub proxy_host: String,
    pub client_ip: String,
    pub proxy_type: String,
    pub proxy_desc: String,
    pub as_name: String,
    pub as_number: u32,
}

impl DebugParams {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        ctx: &EventContext,
        project_component_id: &str,
        component_slug: &str,
        event: &Event,
        method: &String,
        url: &str,
        headers: &HashMap<String, String>,
        body: &str,
        timer: std::time::Instant,
        anonymization: bool,
    ) -> DebugParams {
        DebugParams {
            from: ctx.get_from().clone(),
            project_id: ctx.get_project_id().clone(),
            component_id: project_component_id.to_string(),
            component_slug: component_slug.to_string(),
            event: event.clone(),
            method: method.to_string(),
            url: url.to_string(),
            headers: headers.clone(),
            body: body.to_string(),
            response_content_type: "".to_string(),
            response_status: 500,
            response_body: None,
            timer,
            anonymization,
            incoming_consent: ctx.get_consent().clone(),
            proxy_host: ctx.get_proxy_host().clone(),
            client_ip: ctx.get_ip().clone(),
            proxy_type: ctx.get_proxy_type().clone(),
            proxy_desc: ctx.get_proxy_desc().clone(),
            as_name: ctx.get_as_name().clone(),
            as_number: ctx.get_as_number(),
        }
    }
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DebugComponentRequest {
    pub method: String,
    pub url: String,
    pub headers: Option<HashMap<String, String>>,
    pub body: Option<serde_json::Value>,
}

impl DebugComponentRequest {
    pub fn new(
        method: &String,
        url: &str,
        headers: &HashMap<String, String>,
        body: &str,
    ) -> DebugComponentRequest {
        let content_type = headers
            .iter()
            .find(|(k, _)| k.to_lowercase() == "content-type")
            .map(|(_, v)| v.to_string())
            .unwrap_or_else(|| "text/plain".to_string());

        let b: Option<serde_json::Value> = if body.is_empty() {
            None
        } else if content_type.contains("application/json") || is_body_json(body) {
            Some(serde_json::from_str(body).unwrap_or(serde_json::Value::String(body.to_string())))
        } else {
            Some(serde_json::Value::String(body.to_string()))
        };

        DebugComponentRequest {
            method: method.to_string(),
            url: url.to_string(),
            headers: Some(headers.clone()),
            body: b,
        }
    }
}

fn is_body_json(body: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(body).is_ok()
}

#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DebugComponentResponse {
    pub status_code: i32,
    pub body: Option<serde_json::Value>,
    pub content_type: String,
    pub duration: i32,
}

impl DebugComponentResponse {
    pub fn new(
        status_code: i32,
        body: Option<String>,
        content_type: String,
        duration: i32,
    ) -> DebugComponentResponse {
        let b: Option<serde_json::Value> = if body.is_none() {
            None
        } else if content_type.contains("application/json") {
            Some(
                serde_json::from_str(body.clone().unwrap().as_str())
                    .unwrap_or(serde_json::Value::String(body.clone().unwrap())),
            )
        } else {
            Some(serde_json::Value::String(body.clone().unwrap()))
        };

        DebugComponentResponse {
            status_code,
            body: b,
            content_type,
            duration,
        }
    }
}

pub fn trace_disabled_event(trace: bool, event: &str) {
    if !trace {
        return;
    }

    println!("--------------------------------------------");
    println!(" Event {event} is disabled for this component");
    println!("--------------------------------------------\n");
}

pub fn trace_request(
    trace: bool,
    method: &String,
    url: &String,
    headers: &HeaderMap,
    body: &str,
    outgoing_consent: &String,
    anonymization: bool,
) {
    if !trace {
        return;
    }

    let anonymization_str = if anonymization { "true" } else { "false" };

    println!("-----------");
    println!("  REQUEST  ");
    println!("-----------\n");
    println!("Config:   Consent: {outgoing_consent}, Anonymization: {anonymization_str}");
    println!("Method:   {method}");
    println!("Url:      {url}");
    if !headers.is_empty() {
        print!("Headers:  ");
        for (i, (key, value)) in headers.iter().enumerate() {
            if i == 0 {
                println!("{key}: {value:?}");
            } else {
                println!("          {key}: {value:?}");
            }
        }
    } else {
        println!("Headers:  None");
    }

    if !body.is_empty() {
        println!("Body:");
        let formatter = PrettyFormatter::from_str(body);
        let result = formatter.pretty();
        println!("{result}");
    } else {
        println!("Body:     None");
    }
    println!();
}

pub async fn debug_and_trace_response(
    debug: bool,
    trace: bool,
    params: DebugParams,
    error: String,
) -> anyhow::Result<()> {
    let elapsed = params.timer.elapsed();
    Logger::log_outgoing_event(&params, elapsed.as_millis(), &error);

    if trace {
        println!("------------");
        println!("  RESPONSE  ");
        println!("------------\n");
        println!("Status:   {}", params.response_status);
        println!("Duration: {}ms", elapsed.as_millis());
        if params.response_body.is_some() {
            if let Some(body) = params.response_body.clone() {
                println!("Body:");
                let formatter = PrettyFormatter::from_str(body.as_str());
                let result = formatter.pretty();
                println!("{result}");
            }
        }
        if !error.is_empty() {
            println!("Error:    {}", &error);
        }
        println!();
    }

    if debug {
        let api_super_token = std::env::var("EDGEE_API_SUPER_TOKEN").unwrap_or_default();
        let api_url = std::env::var("EDGEE_API_URL").unwrap_or_default();

        if !api_super_token.is_empty() && !api_url.is_empty() && !params.project_id.is_empty() {
            let api_endpoint = format!("{api_url}/v1/debug/data-collection");
            let debug_entry = DebugPayload::new(params, &error);
            let client = reqwest::Client::builder()
                .timeout(Duration::from_secs(5))
                .build()?;
            let _r = client
                .post(api_endpoint.as_str())
                .header("Content-Type", "application/json")
                .header("Authorization", format!("Bearer {api_super_token}"))
                .body(serde_json::to_string(&debug_entry).unwrap())
                .send()
                .await;
        }
    }

    Ok(())
}