use actix_web::{web, HttpResponse};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
use crate::db::Database;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum AnalyticsEventType {
SessionHarvested,
SessionViewed,
SessionExported,
SessionDeleted,
SessionShared,
SearchPerformed,
ApiRequest,
UserLogin,
UserLogout,
FeatureUsed,
ProviderSync,
ExportCompleted,
}
impl AnalyticsEventType {
pub fn as_str(&self) -> &'static str {
match self {
Self::SessionHarvested => "session_harvested",
Self::SessionViewed => "session_viewed",
Self::SessionExported => "session_exported",
Self::SessionDeleted => "session_deleted",
Self::SessionShared => "session_shared",
Self::SearchPerformed => "search_performed",
Self::ApiRequest => "api_request",
Self::UserLogin => "user_login",
Self::UserLogout => "user_logout",
Self::FeatureUsed => "feature_used",
Self::ProviderSync => "provider_sync",
Self::ExportCompleted => "export_completed",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalyticsEvent {
pub id: String,
pub event_type: AnalyticsEventType,
pub timestamp: DateTime<Utc>,
pub user_id: Option<String>,
pub organization_id: Option<String>,
pub provider: Option<String>,
pub feature: Option<String>,
pub resource_type: Option<String>,
pub resource_id: Option<String>,
pub properties: HashMap<String, serde_json::Value>,
pub client: Option<ClientInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientInfo {
pub platform: Option<String>,
pub app_version: Option<String>,
pub user_agent: Option<String>,
pub ip_address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemStats {
pub total_users: u64,
pub active_users: u64,
pub total_sessions: u64,
pub total_workspaces: u64,
pub storage_used_bytes: u64,
pub storage_total_bytes: u64,
pub uptime_percent: f64,
pub api_requests: u64,
pub error_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageMetrics {
pub period: TimePeriod,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub sessions_harvested: u64,
pub sessions_viewed: u64,
pub sessions_exported: u64,
pub active_users: u64,
pub api_requests: u64,
pub searches: u64,
pub exports: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderStats {
pub provider: String,
pub sessions_count: u64,
pub messages_count: u64,
pub active_users: u64,
pub last_sync: Option<DateTime<Utc>>,
pub sync_success_rate: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserActivitySummary {
pub user_id: String,
pub email: String,
pub sessions_count: u64,
pub last_active: Option<DateTime<Utc>>,
pub api_requests: u64,
pub favorite_provider: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeamStats {
pub organization_id: String,
pub member_count: u64,
pub active_members: u64,
pub total_sessions: u64,
pub total_workspaces: u64,
pub storage_used_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesPoint {
pub timestamp: DateTime<Utc>,
pub value: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeries {
pub metric: String,
pub data: Vec<TimeSeriesPoint>,
pub granularity: Granularity,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TimePeriod {
Today,
Yesterday,
Last7Days,
Last30Days,
Last90Days,
ThisMonth,
LastMonth,
ThisYear,
AllTime,
Custom,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Granularity {
Hour,
Day,
Week,
Month,
}
pub struct AnalyticsService {
db: Database,
}
impl AnalyticsService {
pub fn new(db: Database) -> Self {
Self { db }
}
pub async fn track(&self, event: AnalyticsEvent) -> Result<(), String> {
self.db
.insert_analytics_event(&event)
.map_err(|e| format!("Failed to track event: {}", e))
}
pub async fn track_event(
&self,
event_type: AnalyticsEventType,
user_id: Option<&str>,
organization_id: Option<&str>,
) -> Result<(), String> {
let event = AnalyticsEvent {
id: Uuid::new_v4().to_string(),
event_type,
timestamp: Utc::now(),
user_id: user_id.map(String::from),
organization_id: organization_id.map(String::from),
provider: None,
feature: None,
resource_type: None,
resource_id: None,
properties: HashMap::new(),
client: None,
};
self.track(event).await
}
pub async fn track_session_event(
&self,
event_type: AnalyticsEventType,
user_id: Option<&str>,
session_id: &str,
provider: &str,
) -> Result<(), String> {
let mut properties = HashMap::new();
properties.insert("session_id".to_string(), serde_json::json!(session_id));
let event = AnalyticsEvent {
id: Uuid::new_v4().to_string(),
event_type,
timestamp: Utc::now(),
user_id: user_id.map(String::from),
organization_id: None,
provider: Some(provider.to_string()),
feature: None,
resource_type: Some("session".to_string()),
resource_id: Some(session_id.to_string()),
properties,
client: None,
};
self.track(event).await
}
pub async fn track_api_request(
&self,
user_id: Option<&str>,
method: &str,
path: &str,
status: u16,
duration_ms: u64,
) -> Result<(), String> {
let mut properties = HashMap::new();
properties.insert("method".to_string(), serde_json::json!(method));
properties.insert("path".to_string(), serde_json::json!(path));
properties.insert("status".to_string(), serde_json::json!(status));
properties.insert("duration_ms".to_string(), serde_json::json!(duration_ms));
let event = AnalyticsEvent {
id: Uuid::new_v4().to_string(),
event_type: AnalyticsEventType::ApiRequest,
timestamp: Utc::now(),
user_id: user_id.map(String::from),
organization_id: None,
provider: None,
feature: None,
resource_type: Some("api".to_string()),
resource_id: None,
properties,
client: None,
};
self.track(event).await
}
pub async fn get_system_stats(&self) -> Result<SystemStats, String> {
self.db
.get_system_stats()
.map_err(|e| format!("Failed to get stats: {}", e))
}
pub async fn get_usage_metrics(
&self,
period: TimePeriod,
organization_id: Option<&str>,
) -> Result<UsageMetrics, String> {
let (start, end) = self.period_to_range(period);
self.db
.get_usage_metrics(start, end, organization_id)
.map_err(|e| format!("Failed to get metrics: {}", e))
}
pub async fn get_provider_stats(
&self,
period: TimePeriod,
organization_id: Option<&str>,
) -> Result<Vec<ProviderStats>, String> {
let (start, end) = self.period_to_range(period);
self.db
.get_provider_stats(start, end, organization_id)
.map_err(|e| format!("Failed to get provider stats: {}", e))
}
pub async fn get_top_users(
&self,
period: TimePeriod,
organization_id: Option<&str>,
limit: usize,
) -> Result<Vec<UserActivitySummary>, String> {
let (start, end) = self.period_to_range(period);
self.db
.get_top_users(start, end, organization_id, limit)
.map_err(|e| format!("Failed to get top users: {}", e))
}
pub async fn get_team_stats(&self, organization_id: &str) -> Result<TeamStats, String> {
self.db
.get_team_stats(organization_id)
.map_err(|e| format!("Failed to get team stats: {}", e))
}
pub async fn get_time_series(
&self,
metric: &str,
period: TimePeriod,
granularity: Granularity,
organization_id: Option<&str>,
) -> Result<TimeSeries, String> {
let (start, end) = self.period_to_range(period);
let data = self
.db
.get_time_series(metric, start, end, granularity, organization_id)
.map_err(|e| format!("Failed to get time series: {}", e))?;
Ok(TimeSeries {
metric: metric.to_string(),
data,
granularity,
})
}
pub async fn get_dashboard_summary(
&self,
organization_id: Option<&str>,
) -> Result<DashboardSummary, String> {
let stats = self.get_system_stats().await?;
let usage = self
.get_usage_metrics(TimePeriod::Last7Days, organization_id)
.await?;
let providers = self
.get_provider_stats(TimePeriod::Last30Days, organization_id)
.await?;
let top_users = self
.get_top_users(TimePeriod::Last30Days, organization_id, 10)
.await?;
let sessions_trend = self
.get_time_series(
"sessions",
TimePeriod::Last30Days,
Granularity::Day,
organization_id,
)
.await?;
let users_trend = self
.get_time_series(
"active_users",
TimePeriod::Last30Days,
Granularity::Day,
organization_id,
)
.await?;
Ok(DashboardSummary {
stats,
usage,
providers,
top_users,
sessions_trend,
users_trend,
})
}
pub async fn export_analytics(
&self,
period: TimePeriod,
organization_id: Option<&str>,
format: ExportFormat,
) -> Result<Vec<u8>, String> {
let summary = self.get_dashboard_summary(organization_id).await?;
match format {
ExportFormat::Json => {
serde_json::to_vec_pretty(&summary).map_err(|e| format!("JSON error: {}", e))
}
ExportFormat::Csv => {
let mut output = String::new();
output.push_str("metric,value\n");
output.push_str(&format!("total_users,{}\n", summary.stats.total_users));
output.push_str(&format!("active_users,{}\n", summary.stats.active_users));
output.push_str(&format!(
"total_sessions,{}\n",
summary.stats.total_sessions
));
output.push_str(&format!("api_requests,{}\n", summary.stats.api_requests));
Ok(output.into_bytes())
}
}
}
fn period_to_range(&self, period: TimePeriod) -> (DateTime<Utc>, DateTime<Utc>) {
let now = Utc::now();
let start = match period {
TimePeriod::Today => now
.date_naive()
.and_hms_opt(0, 0, 0)
.map(|dt| DateTime::from_naive_utc_and_offset(dt, Utc))
.unwrap_or(now),
TimePeriod::Yesterday => (now - Duration::days(1))
.date_naive()
.and_hms_opt(0, 0, 0)
.map(|dt| DateTime::from_naive_utc_and_offset(dt, Utc))
.unwrap_or(now),
TimePeriod::Last7Days => now - Duration::days(7),
TimePeriod::Last30Days => now - Duration::days(30),
TimePeriod::Last90Days => now - Duration::days(90),
TimePeriod::ThisMonth => now
.date_naive()
.with_day(1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|dt| DateTime::from_naive_utc_and_offset(dt, Utc))
.unwrap_or(now),
TimePeriod::LastMonth => (now - Duration::days(30))
.date_naive()
.with_day(1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|dt| DateTime::from_naive_utc_and_offset(dt, Utc))
.unwrap_or(now),
TimePeriod::ThisYear => now
.date_naive()
.with_ordinal(1)
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|dt| DateTime::from_naive_utc_and_offset(dt, Utc))
.unwrap_or(now),
TimePeriod::AllTime | TimePeriod::Custom => {
DateTime::from_timestamp(0, 0).unwrap_or(now)
}
};
(start, now)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardSummary {
pub stats: SystemStats,
pub usage: UsageMetrics,
pub providers: Vec<ProviderStats>,
pub top_users: Vec<UserActivitySummary>,
pub sessions_trend: TimeSeries,
pub users_trend: TimeSeries,
}
#[derive(Debug, Clone, Copy, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ExportFormat {
#[default]
Json,
Csv,
}
#[derive(Debug, Deserialize)]
pub struct MetricsQuery {
pub period: Option<TimePeriod>,
pub organization_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct TimeSeriesQuery {
pub metric: String,
pub period: Option<TimePeriod>,
pub granularity: Option<Granularity>,
pub organization_id: Option<String>,
}
pub async fn get_stats(analytics: web::Data<AnalyticsService>) -> HttpResponse {
match analytics.get_system_stats().await {
Ok(stats) => HttpResponse::Ok().json(stats),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_usage(
analytics: web::Data<AnalyticsService>,
query: web::Query<MetricsQuery>,
) -> HttpResponse {
let period = query.period.unwrap_or(TimePeriod::Last30Days);
let org_id = query.organization_id.as_deref();
match analytics.get_usage_metrics(period, org_id).await {
Ok(metrics) => HttpResponse::Ok().json(metrics),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_providers(
analytics: web::Data<AnalyticsService>,
query: web::Query<MetricsQuery>,
) -> HttpResponse {
let period = query.period.unwrap_or(TimePeriod::Last30Days);
let org_id = query.organization_id.as_deref();
match analytics.get_provider_stats(period, org_id).await {
Ok(providers) => HttpResponse::Ok().json(providers),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_users(
analytics: web::Data<AnalyticsService>,
query: web::Query<MetricsQuery>,
) -> HttpResponse {
let period = query.period.unwrap_or(TimePeriod::Last30Days);
let org_id = query.organization_id.as_deref();
match analytics.get_top_users(period, org_id, 50).await {
Ok(users) => HttpResponse::Ok().json(users),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_team(
analytics: web::Data<AnalyticsService>,
path: web::Path<String>,
) -> HttpResponse {
let org_id = path.into_inner();
match analytics.get_team_stats(&org_id).await {
Ok(stats) => HttpResponse::Ok().json(stats),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_timeseries(
analytics: web::Data<AnalyticsService>,
query: web::Query<TimeSeriesQuery>,
) -> HttpResponse {
let period = query.period.unwrap_or(TimePeriod::Last30Days);
let granularity = query.granularity.unwrap_or(Granularity::Day);
let org_id = query.organization_id.as_deref();
match analytics
.get_time_series(&query.metric, period, granularity, org_id)
.await
{
Ok(series) => HttpResponse::Ok().json(series),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn get_dashboard(
analytics: web::Data<AnalyticsService>,
query: web::Query<MetricsQuery>,
) -> HttpResponse {
let org_id = query.organization_id.as_deref();
match analytics.get_dashboard_summary(org_id).await {
Ok(summary) => HttpResponse::Ok().json(summary),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
#[derive(Debug, Deserialize)]
pub struct ExportQuery {
pub period: Option<TimePeriod>,
pub organization_id: Option<String>,
pub format: Option<ExportFormat>,
}
pub async fn export_analytics(
analytics: web::Data<AnalyticsService>,
query: web::Query<ExportQuery>,
) -> HttpResponse {
let period = query.period.unwrap_or(TimePeriod::Last30Days);
let org_id = query.organization_id.as_deref();
let format = query.format.unwrap_or_default();
let content_type = match format {
ExportFormat::Json => "application/json",
ExportFormat::Csv => "text/csv",
};
match analytics.export_analytics(period, org_id, format).await {
Ok(data) => HttpResponse::Ok()
.content_type(content_type)
.append_header((
"Content-Disposition",
"attachment; filename=\"analytics.export\"",
))
.body(data),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub async fn track_event(
analytics: web::Data<AnalyticsService>,
event: web::Json<AnalyticsEvent>,
) -> HttpResponse {
match analytics.track(event.into_inner()).await {
Ok(()) => HttpResponse::Accepted().finish(),
Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({ "error": e })),
}
}
pub fn configure_analytics_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/analytics")
.route("/stats", web::get().to(get_stats))
.route("/usage", web::get().to(get_usage))
.route("/providers", web::get().to(get_providers))
.route("/users", web::get().to(get_users))
.route("/team/{org_id}", web::get().to(get_team))
.route("/timeseries", web::get().to(get_timeseries))
.route("/dashboard", web::get().to(get_dashboard))
.route("/export", web::post().to(export_analytics))
.route("/track", web::post().to(track_event)),
);
}