Skip to content

Commit fc99793

Browse files
committed
feat(analytics): implement analytics dashboard and fix the server side for that thing
1 parent 9509f65 commit fc99793

24 files changed

Lines changed: 1177 additions & 72 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Drop cache_stats table
2+
DROP TABLE cache_stats;
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- Create cache_stats table to persist cache hit/miss counters
2+
CREATE TABLE cache_stats (
3+
id INTEGER PRIMARY KEY NOT NULL,
4+
hit_count BIGINT NOT NULL DEFAULT 0,
5+
miss_count BIGINT NOT NULL DEFAULT 0,
6+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
8+
);
9+
10+
-- Insert initial record
11+
INSERT INTO cache_stats (hit_count, miss_count) VALUES (0, 0);

src/database/analytics.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::connection::{DbPool, get_connection_with_retry};
22
use crate::models::package::*;
33
use crate::schema::{package_files, package_versions, packages};
44
use diesel::prelude::*;
5+
use log::{debug, info};
56

67
/// Analytics and statistics-related database operations
78
pub struct AnalyticsOperations<'a> {
@@ -33,16 +34,30 @@ impl<'a> AnalyticsOperations<'a> {
3334
.map(|(pkg, (ver, file))| (pkg, ver, file))
3435
.collect();
3536

37+
debug!(
38+
"Found {} package files for popular packages calculation",
39+
results.len()
40+
);
41+
3642
let mut package_stats: std::collections::HashMap<String, (i64, i64, i64)> =
3743
std::collections::HashMap::new();
3844

3945
for (pkg, _ver, file) in results {
46+
debug!(
47+
"Processing package: {} with access_count: {}",
48+
pkg.name, file.access_count
49+
);
4050
let entry = package_stats.entry(pkg.name).or_insert((0, 0, 0));
4151
entry.0 += file.access_count as i64; // total downloads
4252
entry.1 += 1; // unique versions
4353
entry.2 += file.size_bytes; // total size
4454
}
4555

56+
info!(
57+
"Aggregated stats for {} unique packages",
58+
package_stats.len()
59+
);
60+
4661
let mut popular_packages: Vec<PopularPackage> = package_stats
4762
.into_iter()
4863
.map(
@@ -58,6 +73,22 @@ impl<'a> AnalyticsOperations<'a> {
5873
popular_packages.sort_by(|a, b| b.total_downloads.cmp(&a.total_downloads));
5974
popular_packages.truncate(limit as usize);
6075

76+
info!(
77+
"Returning {} popular packages (limit: {})",
78+
popular_packages.len(),
79+
limit
80+
);
81+
for (i, pkg) in popular_packages.iter().enumerate() {
82+
debug!(
83+
"Popular package #{}: {} (downloads: {}, versions: {}, size: {} bytes)",
84+
i + 1,
85+
pkg.name,
86+
pkg.total_downloads,
87+
pkg.unique_versions,
88+
pkg.total_size_bytes
89+
);
90+
}
91+
6192
Ok(popular_packages)
6293
}
6394

src/database/cache_stats.rs

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
use super::connection::{DbPool, get_connection_with_retry};
2+
use crate::models::cache::{CacheStatsRecord, NewCacheStatsRecord, UpdateCacheStatsRecord};
3+
use crate::schema::cache_stats;
4+
use chrono::Utc;
5+
use diesel::prelude::*;
6+
7+
/// Cache statistics database operations
8+
pub struct CacheStatsOperations<'a> {
9+
pool: &'a DbPool,
10+
}
11+
12+
impl<'a> CacheStatsOperations<'a> {
13+
pub fn new(pool: &'a DbPool) -> Self {
14+
Self { pool }
15+
}
16+
17+
/// Gets the current cache stats record (there should only be one)
18+
pub fn get_cache_stats(&self) -> Result<Option<CacheStatsRecord>, diesel::result::Error> {
19+
let mut conn = get_connection_with_retry(self.pool).map_err(|e| {
20+
diesel::result::Error::DatabaseError(
21+
diesel::result::DatabaseErrorKind::UnableToSendCommand,
22+
Box::new(e.to_string()),
23+
)
24+
})?;
25+
26+
cache_stats::table
27+
.order(cache_stats::id.desc())
28+
.first::<CacheStatsRecord>(&mut conn)
29+
.optional()
30+
}
31+
32+
/// Updates the cache stats record
33+
pub fn update_cache_stats(
34+
&self,
35+
hit_count: u64,
36+
miss_count: u64,
37+
) -> Result<CacheStatsRecord, diesel::result::Error> {
38+
let mut conn = get_connection_with_retry(self.pool).map_err(|e| {
39+
diesel::result::Error::DatabaseError(
40+
diesel::result::DatabaseErrorKind::UnableToSendCommand,
41+
Box::new(e.to_string()),
42+
)
43+
})?;
44+
45+
let now = Utc::now().naive_utc();
46+
47+
// Try to update existing record first
48+
let update_result = diesel::update(cache_stats::table)
49+
.set(UpdateCacheStatsRecord {
50+
hit_count: Some(hit_count as i64),
51+
miss_count: Some(miss_count as i64),
52+
updated_at: Some(now),
53+
})
54+
.get_result::<CacheStatsRecord>(&mut conn);
55+
56+
match update_result {
57+
Ok(record) => Ok(record),
58+
Err(diesel::result::Error::NotFound) => {
59+
// No record exists, create one
60+
let new_record = NewCacheStatsRecord {
61+
hit_count: hit_count as i64,
62+
miss_count: miss_count as i64,
63+
created_at: now,
64+
updated_at: now,
65+
};
66+
67+
diesel::insert_into(cache_stats::table)
68+
.values(&new_record)
69+
.get_result::<CacheStatsRecord>(&mut conn)
70+
}
71+
Err(e) => Err(e),
72+
}
73+
}
74+
75+
/// Increments hit count
76+
pub fn increment_hit_count(&self) -> Result<(), diesel::result::Error> {
77+
let mut conn = get_connection_with_retry(self.pool).map_err(|e| {
78+
diesel::result::Error::DatabaseError(
79+
diesel::result::DatabaseErrorKind::UnableToSendCommand,
80+
Box::new(e.to_string()),
81+
)
82+
})?;
83+
84+
let now = Utc::now().naive_utc();
85+
86+
// Try to increment existing record
87+
let update_result = diesel::update(cache_stats::table)
88+
.set((
89+
cache_stats::hit_count.eq(cache_stats::hit_count + 1),
90+
cache_stats::updated_at.eq(now),
91+
))
92+
.execute(&mut conn);
93+
94+
match update_result {
95+
Ok(0) => {
96+
// No record exists, create one with hit_count = 1
97+
let new_record = NewCacheStatsRecord {
98+
hit_count: 1,
99+
miss_count: 0,
100+
created_at: now,
101+
updated_at: now,
102+
};
103+
104+
diesel::insert_into(cache_stats::table)
105+
.values(&new_record)
106+
.execute(&mut conn)?;
107+
}
108+
Ok(_) => {} // Successfully updated
109+
Err(e) => return Err(e),
110+
}
111+
112+
Ok(())
113+
}
114+
115+
/// Increments miss count
116+
pub fn increment_miss_count(&self) -> Result<(), diesel::result::Error> {
117+
let mut conn = get_connection_with_retry(self.pool).map_err(|e| {
118+
diesel::result::Error::DatabaseError(
119+
diesel::result::DatabaseErrorKind::UnableToSendCommand,
120+
Box::new(e.to_string()),
121+
)
122+
})?;
123+
124+
let now = Utc::now().naive_utc();
125+
126+
// Try to increment existing record
127+
let update_result = diesel::update(cache_stats::table)
128+
.set((
129+
cache_stats::miss_count.eq(cache_stats::miss_count + 1),
130+
cache_stats::updated_at.eq(now),
131+
))
132+
.execute(&mut conn);
133+
134+
match update_result {
135+
Ok(0) => {
136+
// No record exists, create one with miss_count = 1
137+
let new_record = NewCacheStatsRecord {
138+
hit_count: 0,
139+
miss_count: 1,
140+
created_at: now,
141+
updated_at: now,
142+
};
143+
144+
diesel::insert_into(cache_stats::table)
145+
.values(&new_record)
146+
.execute(&mut conn)?;
147+
}
148+
Ok(_) => {} // Successfully updated
149+
Err(e) => return Err(e),
150+
}
151+
152+
Ok(())
153+
}
154+
}

src/database/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
//! This module is organized into several sub-modules:
44
//! - `connection`: Database connection management and pool configuration
55
//! - `packages`: Package-related database operations
6-
//! - `versions`: Package version-related database operations
6+
//! - `versions`: Package version-related database operations
77
//! - `files`: Package file-related database operations
88
//! - `analytics`: Analytics and statistics operations
9+
//! - `cache_stats`: Cache statistics operations
910
//! - `service`: Main DatabaseService that provides a unified interface
1011
1112
pub mod analytics;
13+
pub mod cache_stats;
1214
pub mod connection;
1315
pub mod files;
1416
pub mod packages;
@@ -21,6 +23,7 @@ pub use service::DatabaseService;
2123

2224
// Re-export operation structs for advanced usage
2325
pub use analytics::AnalyticsOperations;
26+
pub use cache_stats::CacheStatsOperations;
2427
pub use files::FileOperations;
2528
pub use packages::PackageOperations;
2629
pub use versions::VersionOperations;

src/database/service.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::analytics::AnalyticsOperations;
2+
use super::cache_stats::CacheStatsOperations;
23
use super::connection::{DbConnection, DbPool, create_pool, get_connection_with_retry};
34
use super::files::{CompletePackageParams, FileOperations, PackageFileParams};
45
use super::packages::PackageOperations;
@@ -152,4 +153,31 @@ impl DatabaseService {
152153
let ops = AnalyticsOperations::new(&self.pool);
153154
ops.get_cache_stats()
154155
}
156+
157+
// Cache stats operations
158+
pub fn get_persistent_cache_stats(
159+
&self,
160+
) -> Result<Option<crate::models::cache::CacheStatsRecord>, diesel::result::Error> {
161+
let ops = CacheStatsOperations::new(&self.pool);
162+
ops.get_cache_stats()
163+
}
164+
165+
pub fn update_persistent_cache_stats(
166+
&self,
167+
hit_count: u64,
168+
miss_count: u64,
169+
) -> Result<crate::models::cache::CacheStatsRecord, diesel::result::Error> {
170+
let ops = CacheStatsOperations::new(&self.pool);
171+
ops.update_cache_stats(hit_count, miss_count)
172+
}
173+
174+
pub fn increment_cache_hit_count(&self) -> Result<(), diesel::result::Error> {
175+
let ops = CacheStatsOperations::new(&self.pool);
176+
ops.increment_hit_count()
177+
}
178+
179+
pub fn increment_cache_miss_count(&self) -> Result<(), diesel::result::Error> {
180+
let ops = CacheStatsOperations::new(&self.pool);
181+
ops.increment_miss_count()
182+
}
155183
}

src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ pub fn create_rocket() -> rocket::Rocket<rocket::Build> {
2525
// Create HTTP client
2626
let client = reqwest::Client::new();
2727

28-
// Initialize cache service
29-
let cache = Arc::new(CacheService::new(config.clone()).expect("Failed to initialize cache"));
30-
31-
// Initialize database service
28+
// Initialize database service first
3229
let database = Arc::new(
3330
DatabaseService::new(&config.database_url).expect("Failed to initialize database"),
3431
);
3532

33+
// Initialize cache service with database for persistent stats
34+
let cache = Arc::new(
35+
CacheService::new_with_database(config.clone(), Some(&database))
36+
.expect("Failed to initialize cache"),
37+
);
38+
3639
// Create app state
3740
let state = AppState {
3841
config,

src/models/cache.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
use crate::models::package::{PackageWithVersions, PopularPackage};
2+
use crate::schema::cache_stats;
3+
use chrono::NaiveDateTime;
4+
use diesel::prelude::*;
25
use rocket::serde::Serialize;
36

47
#[derive(Debug, Clone)]
@@ -27,6 +30,35 @@ pub struct CacheAnalytics {
2730
pub cache_hit_rate: f64,
2831
}
2932

33+
// Database model for persistent cache stats
34+
#[derive(Queryable, Selectable, Serialize, Debug, Clone)]
35+
#[diesel(table_name = cache_stats)]
36+
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
37+
pub struct CacheStatsRecord {
38+
pub id: i32,
39+
pub hit_count: i64,
40+
pub miss_count: i64,
41+
pub created_at: NaiveDateTime,
42+
pub updated_at: NaiveDateTime,
43+
}
44+
45+
#[derive(Insertable, Debug)]
46+
#[diesel(table_name = cache_stats)]
47+
pub struct NewCacheStatsRecord {
48+
pub hit_count: i64,
49+
pub miss_count: i64,
50+
pub created_at: NaiveDateTime,
51+
pub updated_at: NaiveDateTime,
52+
}
53+
54+
#[derive(AsChangeset, Debug)]
55+
#[diesel(table_name = cache_stats)]
56+
pub struct UpdateCacheStatsRecord {
57+
pub hit_count: Option<i64>,
58+
pub miss_count: Option<i64>,
59+
pub updated_at: Option<NaiveDateTime>,
60+
}
61+
3062
#[derive(Serialize)]
3163
pub struct CacheStatsResponse {
3264
pub enabled: bool,

0 commit comments

Comments
 (0)