Skip to main content

systemprompt_analytics/models/
events.rs

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