Skip to main content

systemprompt_models/events/
analytics_event.rs

1//! Analytics events emitted over the event bus and SSE streams.
2//!
3//! [`AnalyticsEvent`] is the timestamped, tagged union of session, page-view,
4//! engagement, and real-time-stats events; each carries a typed payload
5//! struct. [`AnalyticsEventBuilder`] stamps each variant with the current
6//! time at construction.
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use systemprompt_identifiers::{ContentId, SessionId, UserId};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
14pub enum AnalyticsEvent {
15    SessionStarted {
16        timestamp: DateTime<Utc>,
17        #[serde(flatten)]
18        payload: SessionStartedPayload,
19    },
20    SessionEnded {
21        timestamp: DateTime<Utc>,
22        #[serde(flatten)]
23        payload: SessionEndedPayload,
24    },
25    PageView {
26        timestamp: DateTime<Utc>,
27        #[serde(flatten)]
28        payload: PageViewPayload,
29    },
30    EngagementUpdate {
31        timestamp: DateTime<Utc>,
32        #[serde(flatten)]
33        payload: EngagementUpdatePayload,
34    },
35    RealTimeStats {
36        timestamp: DateTime<Utc>,
37        #[serde(flatten)]
38        payload: RealTimeStatsPayload,
39    },
40    Heartbeat {
41        timestamp: DateTime<Utc>,
42    },
43}
44
45impl AnalyticsEvent {
46    pub const fn timestamp(&self) -> DateTime<Utc> {
47        match self {
48            Self::SessionStarted { timestamp, .. }
49            | Self::SessionEnded { timestamp, .. }
50            | Self::PageView { timestamp, .. }
51            | Self::EngagementUpdate { timestamp, .. }
52            | Self::RealTimeStats { timestamp, .. }
53            | Self::Heartbeat { timestamp } => *timestamp,
54        }
55    }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SessionStartedPayload {
60    pub session_id: SessionId,
61    pub device_type: Option<String>,
62    pub browser: Option<String>,
63    pub os: Option<String>,
64    pub country: Option<String>,
65    pub referrer_source: Option<String>,
66    pub is_bot: bool,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct SessionEndedPayload {
71    pub session_id: SessionId,
72    pub duration_ms: i64,
73    pub page_count: i64,
74    pub request_count: i64,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PageViewPayload {
79    pub session_id: SessionId,
80    pub user_id: Option<UserId>,
81    pub page_url: String,
82    pub content_id: Option<ContentId>,
83    pub referrer: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct EngagementUpdatePayload {
88    pub session_id: SessionId,
89    pub page_url: String,
90    pub scroll_depth: i32,
91    pub time_on_page_ms: i64,
92    pub click_count: i32,
93}
94
95#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
96pub struct RealTimeStatsPayload {
97    pub active_sessions: i64,
98    pub active_users: i64,
99    pub requests_per_minute: i64,
100    pub page_views_last_5m: i64,
101    pub bot_requests_last_5m: i64,
102}
103
104#[derive(Debug, Clone, Copy)]
105pub struct AnalyticsEventBuilder;
106
107impl AnalyticsEventBuilder {
108    pub fn session_started(payload: SessionStartedPayload) -> AnalyticsEvent {
109        AnalyticsEvent::SessionStarted {
110            timestamp: Utc::now(),
111            payload,
112        }
113    }
114
115    pub fn session_ended(
116        session_id: SessionId,
117        duration_ms: i64,
118        page_count: i64,
119        request_count: i64,
120    ) -> AnalyticsEvent {
121        AnalyticsEvent::SessionEnded {
122            timestamp: Utc::now(),
123            payload: SessionEndedPayload {
124                session_id,
125                duration_ms,
126                page_count,
127                request_count,
128            },
129        }
130    }
131
132    pub fn page_view(
133        session_id: SessionId,
134        user_id: Option<UserId>,
135        page_url: String,
136        content_id: Option<ContentId>,
137        referrer: Option<String>,
138    ) -> AnalyticsEvent {
139        AnalyticsEvent::PageView {
140            timestamp: Utc::now(),
141            payload: PageViewPayload {
142                session_id,
143                user_id,
144                page_url,
145                content_id,
146                referrer,
147            },
148        }
149    }
150
151    pub fn engagement_update(
152        session_id: SessionId,
153        page_url: String,
154        scroll_depth: i32,
155        time_on_page_ms: i64,
156        click_count: i32,
157    ) -> AnalyticsEvent {
158        AnalyticsEvent::EngagementUpdate {
159            timestamp: Utc::now(),
160            payload: EngagementUpdatePayload {
161                session_id,
162                page_url,
163                scroll_depth,
164                time_on_page_ms,
165                click_count,
166            },
167        }
168    }
169
170    pub fn realtime_stats(
171        active_sessions: i64,
172        active_users: i64,
173        requests_per_minute: i64,
174        page_views_last_5m: i64,
175        bot_requests_last_5m: i64,
176    ) -> AnalyticsEvent {
177        AnalyticsEvent::RealTimeStats {
178            timestamp: Utc::now(),
179            payload: RealTimeStatsPayload {
180                active_sessions,
181                active_users,
182                requests_per_minute,
183                page_views_last_5m,
184                bot_requests_last_5m,
185            },
186        }
187    }
188
189    pub fn heartbeat() -> AnalyticsEvent {
190        AnalyticsEvent::Heartbeat {
191            timestamp: Utc::now(),
192        }
193    }
194}