Skip to main content

tuitbot_core/automation/analytics_loop/
collector.rs

1//! Port traits and data types for analytics collection.
2//!
3//! Defines the interfaces for fetching profile/engagement metrics
4//! and storing analytics data.
5
6/// Fetches the authenticated user's profile metrics.
7#[async_trait::async_trait]
8pub trait ProfileFetcher: Send + Sync {
9    /// Get current follower count, following count, and tweet count.
10    async fn get_profile_metrics(&self) -> Result<ProfileMetrics, AnalyticsError>;
11}
12
13/// Fetches engagement metrics for a specific tweet.
14#[async_trait::async_trait]
15pub trait EngagementFetcher: Send + Sync {
16    /// Get engagement metrics for a tweet by its ID.
17    async fn get_tweet_metrics(&self, tweet_id: &str) -> Result<TweetMetrics, AnalyticsError>;
18}
19
20/// Storage operations for analytics data.
21#[async_trait::async_trait]
22pub trait AnalyticsStorage: Send + Sync {
23    /// Store a daily follower snapshot.
24    async fn store_follower_snapshot(
25        &self,
26        followers: i64,
27        following: i64,
28        tweets: i64,
29    ) -> Result<(), AnalyticsError>;
30
31    /// Get yesterday's follower count (for drop detection).
32    async fn get_yesterday_followers(&self) -> Result<Option<i64>, AnalyticsError>;
33
34    /// Get reply IDs posted approximately 24h ago that need performance measurement.
35    async fn get_replies_needing_measurement(&self) -> Result<Vec<String>, AnalyticsError>;
36
37    /// Get tweet IDs posted approximately 24h ago that need performance measurement.
38    async fn get_tweets_needing_measurement(&self) -> Result<Vec<String>, AnalyticsError>;
39
40    /// Store reply performance metrics.
41    async fn store_reply_performance(
42        &self,
43        reply_id: &str,
44        likes: i64,
45        replies: i64,
46        impressions: i64,
47        score: f64,
48    ) -> Result<(), AnalyticsError>;
49
50    /// Store tweet performance metrics.
51    async fn store_tweet_performance(
52        &self,
53        tweet_id: &str,
54        likes: i64,
55        retweets: i64,
56        replies: i64,
57        impressions: i64,
58        score: f64,
59    ) -> Result<(), AnalyticsError>;
60
61    /// Update the content score running average for a topic.
62    async fn update_content_score(
63        &self,
64        topic: &str,
65        format: &str,
66        score: f64,
67    ) -> Result<(), AnalyticsError>;
68
69    /// Log an action.
70    async fn log_action(
71        &self,
72        action_type: &str,
73        status: &str,
74        message: &str,
75    ) -> Result<(), AnalyticsError>;
76
77    /// Run Forge sync if the active content source has analytics sync enabled.
78    ///
79    /// Returns `Ok(Some(summary))` when sync ran, `Ok(None)` when disabled
80    /// or not applicable, `Err` on failure.
81    async fn run_forge_sync_if_enabled(&self) -> Result<Option<ForgeSyncResult>, AnalyticsError> {
82        // Default: no Forge sync (backwards-compatible for existing impls).
83        Ok(None)
84    }
85
86    /// Run background aggregation jobs (best-times heatmap, reach snapshots).
87    ///
88    /// Called at the end of each analytics iteration. Default is a no-op so
89    /// existing impls don't break.
90    async fn run_aggregations(&self) -> Result<(), AnalyticsError> {
91        Ok(())
92    }
93}
94
95// ============================================================================
96// Types
97// ============================================================================
98
99/// Profile metrics from X API.
100#[derive(Debug, Clone)]
101pub struct ProfileMetrics {
102    pub follower_count: i64,
103    pub following_count: i64,
104    pub tweet_count: i64,
105}
106
107/// Tweet engagement metrics from X API.
108#[derive(Debug, Clone)]
109pub struct TweetMetrics {
110    pub likes: i64,
111    pub retweets: i64,
112    pub replies: i64,
113    pub impressions: i64,
114}
115
116/// Analytics-specific errors.
117#[derive(Debug)]
118pub enum AnalyticsError {
119    /// X API error.
120    ApiError(String),
121    /// Storage error.
122    StorageError(String),
123    /// Other error.
124    Other(String),
125}
126
127impl std::fmt::Display for AnalyticsError {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        match self {
130            Self::ApiError(msg) => write!(f, "API error: {msg}"),
131            Self::StorageError(msg) => write!(f, "storage error: {msg}"),
132            Self::Other(msg) => write!(f, "{msg}"),
133        }
134    }
135}
136
137impl std::error::Error for AnalyticsError {}
138
139/// Result of a Forge sync iteration (returned by `run_forge_sync_if_enabled`).
140#[derive(Debug, Default, Clone)]
141pub struct ForgeSyncResult {
142    pub tweets_synced: usize,
143    pub threads_synced: usize,
144}