Skip to main content

systemprompt_analytics/models/
events.rs

1use serde::{Deserialize, Serialize};
2use systemprompt_identifiers::ContentId;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum AnalyticsEventType {
7    PageView,
8    PageExit,
9    LinkClick,
10    Scroll,
11    Engagement,
12    Conversion,
13    #[serde(untagged)]
14    Custom(String),
15}
16
17impl AnalyticsEventType {
18    pub fn as_str(&self) -> &str {
19        match self {
20            Self::PageView => "page_view",
21            Self::PageExit => "page_exit",
22            Self::LinkClick => "link_click",
23            Self::Scroll => "scroll",
24            Self::Engagement => "engagement",
25            Self::Conversion => "conversion",
26            Self::Custom(s) => s.as_str(),
27        }
28    }
29
30    pub const fn category(&self) -> &str {
31        match self {
32            Self::PageView | Self::PageExit => "navigation",
33            Self::LinkClick => "interaction",
34            Self::Scroll | Self::Engagement => "engagement",
35            Self::Conversion => "conversion",
36            Self::Custom(_) => "custom",
37        }
38    }
39}
40
41impl std::fmt::Display for AnalyticsEventType {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}", self.as_str())
44    }
45}
46
47#[derive(Debug, Clone, Deserialize)]
48pub struct CreateAnalyticsEventInput {
49    pub event_type: AnalyticsEventType,
50    pub page_url: String,
51    #[serde(default)]
52    pub content_id: Option<ContentId>,
53    #[serde(default)]
54    pub slug: Option<String>,
55    #[serde(default)]
56    pub referrer: Option<String>,
57    #[serde(default)]
58    pub data: Option<serde_json::Value>,
59}
60
61#[derive(Debug, Clone, Deserialize)]
62pub struct CreateAnalyticsEventBatchInput {
63    pub events: Vec<CreateAnalyticsEventInput>,
64}
65
66#[derive(Debug, Clone, Serialize)]
67pub struct AnalyticsEventCreated {
68    // SQLx: analytics event primary key (text column, no typed ID defined)
69    pub id: String,
70    pub event_type: String,
71}
72
73#[derive(Debug, Clone, Serialize)]
74pub struct AnalyticsEventBatchResponse {
75    pub recorded: usize,
76    pub events: Vec<AnalyticsEventCreated>,
77}
78
79#[derive(Debug, Clone, Default, Serialize, Deserialize)]
80pub struct EngagementEventData {
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub max_scroll_depth: Option<i32>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub time_on_page_ms: Option<i64>,
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub time_to_first_interaction_ms: Option<i64>,
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub time_to_first_scroll_ms: Option<i64>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub click_count: Option<i32>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub scroll_velocity_avg: Option<f32>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub scroll_direction_changes: Option<i32>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub mouse_move_distance_px: Option<i32>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub keyboard_events: Option<i32>,
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub copy_events: Option<i32>,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub focus_time_ms: Option<i32>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub blur_count: Option<i32>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub tab_switches: Option<i32>,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub visible_time_ms: Option<i64>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub hidden_time_ms: Option<i64>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub is_rage_click: Option<bool>,
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub is_dead_click: Option<bool>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub reading_pattern: Option<String>,
117}
118
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct LinkClickEventData {
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub target_url: Option<String>,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub link_text: Option<String>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub link_position: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub is_external: Option<bool>,
129}
130
131#[derive(Debug, Clone, Default, Serialize, Deserialize)]
132pub struct ScrollEventData {
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub depth: Option<i32>,
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub milestone: Option<i32>,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub direction: Option<String>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub velocity: Option<f32>,
141}
142
143#[derive(Debug, Clone, Default, Serialize, Deserialize)]
144pub struct ConversionEventData {
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub goal_name: Option<String>,
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub goal_value: Option<f64>,
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub funnel_step: Option<i32>,
151}