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