systemprompt-analytics 0.2.1

Analytics for systemprompt.io AI governance infrastructure. Session, agent, tool, and microdollar-precision cost attribution across the MCP governance pipeline.
Documentation
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::ContentId;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnalyticsEventType {
    PageView,
    PageExit,
    LinkClick,
    Scroll,
    Engagement,
    Conversion,
    #[serde(untagged)]
    Custom(String),
}

impl AnalyticsEventType {
    pub fn as_str(&self) -> &str {
        match self {
            Self::PageView => "page_view",
            Self::PageExit => "page_exit",
            Self::LinkClick => "link_click",
            Self::Scroll => "scroll",
            Self::Engagement => "engagement",
            Self::Conversion => "conversion",
            Self::Custom(s) => s.as_str(),
        }
    }

    pub const fn category(&self) -> &str {
        match self {
            Self::PageView | Self::PageExit => "navigation",
            Self::LinkClick => "interaction",
            Self::Scroll | Self::Engagement => "engagement",
            Self::Conversion => "conversion",
            Self::Custom(_) => "custom",
        }
    }
}

impl std::fmt::Display for AnalyticsEventType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct CreateAnalyticsEventInput {
    pub event_type: AnalyticsEventType,
    pub page_url: String,
    #[serde(default)]
    pub content_id: Option<ContentId>,
    #[serde(default)]
    pub slug: Option<String>,
    #[serde(default)]
    pub referrer: Option<String>,
    #[serde(default)]
    pub data: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct CreateAnalyticsEventBatchInput {
    pub events: Vec<CreateAnalyticsEventInput>,
}

#[derive(Debug, Clone, Serialize)]
pub struct AnalyticsEventCreated {
    // SQLx: analytics event primary key (text column, no typed ID defined)
    pub id: String,
    pub event_type: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct AnalyticsEventBatchResponse {
    pub recorded: usize,
    pub events: Vec<AnalyticsEventCreated>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EngagementEventData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_scroll_depth: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub time_on_page_ms: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub time_to_first_interaction_ms: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub time_to_first_scroll_ms: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub click_count: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scroll_velocity_avg: Option<f32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scroll_direction_changes: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub mouse_move_distance_px: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub keyboard_events: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub copy_events: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub focus_time_ms: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blur_count: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tab_switches: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub visible_time_ms: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hidden_time_ms: Option<i64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_rage_click: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_dead_click: Option<bool>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reading_pattern: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LinkClickEventData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub link_text: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub link_position: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_external: Option<bool>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ScrollEventData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub depth: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub milestone: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub direction: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub velocity: Option<f32>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConversionEventData {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub goal_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub goal_value: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub funnel_step: Option<i32>,
}