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