systemprompt_content/services/link/
analytics.rs1use crate::error::ContentError;
2use crate::models::{
3 CampaignLink, CampaignPerformance, ContentJourneyNode, LinkClick, LinkPerformance,
4 RecordClickParams, TrackClickParams,
5};
6use crate::repository::{LinkAnalyticsRepository, LinkRepository};
7use chrono::Utc;
8use systemprompt_database::DbPool;
9use systemprompt_identifiers::{CampaignId, ContentId, LinkClickId, LinkId};
10
11const DEFAULT_JOURNEY_LIMIT: i64 = 50;
12const DEFAULT_CLICKS_LIMIT: i64 = 100;
13
14#[derive(Debug)]
15pub struct LinkAnalyticsService {
16 link_repo: LinkRepository,
17 analytics_repo: LinkAnalyticsRepository,
18}
19
20impl LinkAnalyticsService {
21 pub fn new(db: &DbPool) -> Result<Self, ContentError> {
22 Ok(Self {
23 link_repo: LinkRepository::new(db)?,
24 analytics_repo: LinkAnalyticsRepository::new(db)?,
25 })
26 }
27
28 pub async fn track_click(&self, params: &TrackClickParams) -> Result<LinkClick, ContentError> {
29 let is_first_click = !self
30 .analytics_repo
31 .check_session_clicked_link(¶ms.link_id, ¶ms.session_id)
32 .await?;
33
34 let click_id = LinkClickId::generate();
35 let clicked_at = Utc::now();
36
37 let record_params = RecordClickParams::new(
38 click_id.clone(),
39 params.link_id.clone(),
40 params.session_id.clone(),
41 clicked_at,
42 )
43 .with_user_id(params.user_id.clone())
44 .with_context_id(params.context_id.clone())
45 .with_task_id(params.task_id.clone())
46 .with_referrer_page(params.referrer_page.clone())
47 .with_referrer_url(params.referrer_url.clone())
48 .with_user_agent(params.user_agent.clone())
49 .with_ip_address(params.ip_address.clone())
50 .with_device_type(params.device_type.clone())
51 .with_country(params.country.clone())
52 .with_is_first_click(is_first_click)
53 .with_is_conversion(false);
54
55 self.analytics_repo.record_click(&record_params).await?;
56
57 self.analytics_repo
58 .increment_link_clicks(¶ms.link_id, is_first_click)
59 .await?;
60
61 Ok(LinkClick {
62 id: click_id,
63 link_id: params.link_id.clone(),
64 session_id: params.session_id.clone(),
65 user_id: params.user_id.clone(),
66 context_id: params.context_id.clone(),
67 task_id: params.task_id.clone(),
68 referrer_page: params.referrer_page.clone(),
69 referrer_url: params.referrer_url.clone(),
70 clicked_at: Some(clicked_at),
71 user_agent: params.user_agent.clone(),
72 ip_address: params.ip_address.clone(),
73 device_type: params.device_type.clone(),
74 country: params.country.clone(),
75 is_first_click: Some(is_first_click),
76 is_conversion: Some(false),
77 conversion_at: None,
78 time_on_page_seconds: None,
79 scroll_depth_percent: None,
80 })
81 }
82
83 pub async fn get_link_performance(
84 &self,
85 link_id: &LinkId,
86 ) -> Result<Option<LinkPerformance>, ContentError> {
87 Ok(self.analytics_repo.get_link_performance(link_id).await?)
88 }
89
90 pub async fn get_campaign_performance(
91 &self,
92 campaign_id: &CampaignId,
93 ) -> Result<Option<CampaignPerformance>, ContentError> {
94 Ok(self
95 .analytics_repo
96 .get_campaign_performance(campaign_id)
97 .await?)
98 }
99
100 pub async fn get_content_journey_map(
101 &self,
102 limit: Option<i64>,
103 offset: Option<i64>,
104 ) -> Result<Vec<ContentJourneyNode>, ContentError> {
105 let limit = limit.unwrap_or(DEFAULT_JOURNEY_LIMIT);
106 let offset = offset.unwrap_or(0);
107 Ok(self
108 .analytics_repo
109 .get_content_journey_map(limit, offset)
110 .await?)
111 }
112
113 pub async fn get_link_clicks(
114 &self,
115 link_id: &LinkId,
116 limit: Option<i64>,
117 offset: Option<i64>,
118 ) -> Result<Vec<LinkClick>, ContentError> {
119 let limit = limit.unwrap_or(DEFAULT_CLICKS_LIMIT);
120 let offset = offset.unwrap_or(0);
121 Ok(self
122 .analytics_repo
123 .get_clicks_by_link(link_id, limit, offset)
124 .await?)
125 }
126
127 pub async fn get_links_by_campaign(
128 &self,
129 campaign_id: &CampaignId,
130 ) -> Result<Vec<CampaignLink>, ContentError> {
131 Ok(self.link_repo.list_links_by_campaign(campaign_id).await?)
132 }
133
134 pub async fn get_links_by_source_content(
135 &self,
136 source_content_id: &ContentId,
137 ) -> Result<Vec<CampaignLink>, ContentError> {
138 Ok(self
139 .link_repo
140 .list_links_by_source_content(source_content_id)
141 .await?)
142 }
143}