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