Skip to content

Commit 63dd5cd

Browse files
committed
refactor(digest): Update notification digest structure and enhance project statistics
- Removed unused security and resource fields from DigestSections and WeeklyDigestData. - Introduced ProjectStats struct to encapsulate detailed project metrics including visitors, page views, and deployments. - Updated the DigestService to aggregate project data and calculate week-over-week changes. - Enhanced the digest templates to include a new Projects section, displaying project activity metrics in both HTML and text formats. - Refactored the scheduler to improve digest sending logic and added utility functions for time parsing.
1 parent cec29ae commit 63dd5cd

File tree

9 files changed

+820
-272
lines changed

9 files changed

+820
-272
lines changed

crates/temps-notifications/src/digest/digest_data.rs

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ pub struct DigestSections {
1212
pub deployments: bool,
1313
pub errors: bool,
1414
pub funnels: bool,
15-
pub security: bool,
16-
pub resources: bool,
15+
pub projects: bool,
1716
}
1817

1918
impl Default for DigestSections {
@@ -23,8 +22,7 @@ impl Default for DigestSections {
2322
deployments: true,
2423
errors: true,
2524
funnels: true,
26-
security: true,
27-
resources: true,
25+
projects: true,
2826
}
2927
}
3028
}
@@ -40,8 +38,7 @@ pub struct WeeklyDigestData {
4038
pub deployments: Option<DeploymentData>,
4139
pub errors: Option<ErrorData>,
4240
pub funnels: Option<FunnelData>,
43-
pub security: Option<SecurityData>,
44-
pub resources: Option<ResourceData>,
41+
pub projects: Vec<ProjectStats>,
4542
}
4643

4744
/// Executive summary with key metrics
@@ -166,17 +163,17 @@ pub struct SuspiciousActivity {
166163
pub description: String,
167164
}
168165

169-
/// Resource and cost data
166+
/// Individual project statistics
170167
#[derive(Debug, Clone, Serialize, Deserialize)]
171-
pub struct ResourceData {
172-
pub storage_used_mb: f64,
173-
pub storage_growth_mb: f64,
174-
pub database_size_mb: f64,
175-
pub database_growth_mb: f64,
176-
pub log_volume_mb: f64,
177-
pub backup_count: i64,
178-
pub backup_size_mb: f64,
179-
pub recommendations: Vec<String>,
168+
pub struct ProjectStats {
169+
pub project_id: i32,
170+
pub project_name: String,
171+
pub project_slug: String,
172+
pub visitors: i64,
173+
pub page_views: i64,
174+
pub unique_sessions: i64,
175+
pub deployments: i64,
176+
pub week_over_week_change: f64,
180177
}
181178

182179
impl WeeklyDigestData {
@@ -198,8 +195,7 @@ impl WeeklyDigestData {
198195
deployments: None,
199196
errors: None,
200197
funnels: None,
201-
security: None,
202-
resources: None,
198+
projects: Vec::new(),
203199
}
204200
}
205201

@@ -209,7 +205,6 @@ impl WeeklyDigestData {
209205
|| self.deployments.is_some()
210206
|| self.errors.is_some()
211207
|| self.funnels.is_some()
212-
|| self.security.is_some()
213-
|| self.resources.is_some()
208+
|| !self.projects.is_empty()
214209
}
215210
}

crates/temps-notifications/src/digest/digest_service.rs

Lines changed: 153 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ use sea_orm::{
99
ColumnTrait, DatabaseConnection, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
1010
QuerySelect,
1111
};
12-
use std::collections::HashMap;
1312
use std::sync::Arc;
14-
use temps_entities::{audit_logs, deployments, events, projects, users};
13+
use temps_entities::{deployments, events, projects};
1514
use tracing::{error, info};
1615

1716
pub struct DigestService {
@@ -92,18 +91,11 @@ impl DigestService {
9291
digest.funnels = self.aggregate_funnel_data(week_start, week_end).await.ok();
9392
}
9493

95-
if sections.security {
96-
digest.security = self
97-
.aggregate_security_data(week_start, week_end)
94+
if sections.projects {
95+
digest.projects = self
96+
.aggregate_project_data(week_start, week_end)
9897
.await
99-
.ok();
100-
}
101-
102-
if sections.resources {
103-
digest.resources = self
104-
.aggregate_resource_data(week_start, week_end)
105-
.await
106-
.ok();
98+
.unwrap_or_default();
10799
}
108100

109101
// Build executive summary
@@ -257,55 +249,87 @@ impl DigestService {
257249
})
258250
}
259251

260-
/// Aggregate security and access data
261-
async fn aggregate_security_data(
252+
/// Aggregate individual project statistics
253+
async fn aggregate_project_data(
262254
&self,
263255
week_start: DateTime<Utc>,
264256
week_end: DateTime<Utc>,
265-
) -> Result<SecurityData> {
266-
// Count new user signups
267-
let new_user_signups = users::Entity::find()
268-
.filter(users::Column::CreatedAt.between(week_start, week_end))
269-
.count(self.db.as_ref())
270-
.await? as i64;
271-
272-
// Aggregate audit logs by operation type
273-
let audit_logs_list = audit_logs::Entity::find()
274-
.filter(audit_logs::Column::CreatedAt.between(week_start, week_end))
275-
.all(self.db.as_ref())
276-
.await?;
257+
) -> Result<Vec<ProjectStats>> {
258+
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect};
259+
use temps_entities::{deployments, events, projects};
260+
261+
// Get all projects
262+
let all_projects = projects::Entity::find().all(self.db.as_ref()).await?;
263+
264+
let mut project_stats = Vec::new();
265+
266+
for project in all_projects {
267+
// Count unique sessions for this project
268+
let visitors = events::Entity::find()
269+
.filter(events::Column::ProjectId.eq(project.id))
270+
.filter(events::Column::Timestamp.between(week_start, week_end))
271+
.filter(events::Column::SessionId.is_not_null())
272+
.select_only()
273+
.column(events::Column::SessionId)
274+
.distinct()
275+
.count(self.db.as_ref())
276+
.await? as i64;
277+
278+
// Count page views for this project
279+
let page_views = events::Entity::find()
280+
.filter(events::Column::ProjectId.eq(project.id))
281+
.filter(events::Column::Timestamp.between(week_start, week_end))
282+
.count(self.db.as_ref())
283+
.await? as i64;
284+
285+
// Count deployments for this project
286+
let deployment_count = deployments::Entity::find()
287+
.filter(deployments::Column::ProjectId.eq(project.id))
288+
.filter(deployments::Column::CreatedAt.between(week_start, week_end))
289+
.count(self.db.as_ref())
290+
.await? as i64;
291+
292+
// Calculate previous week visitors for trend
293+
let prev_week_start = week_start - Duration::days(7);
294+
let prev_week_end = week_start;
295+
296+
let prev_visitors = events::Entity::find()
297+
.filter(events::Column::ProjectId.eq(project.id))
298+
.filter(events::Column::Timestamp.between(prev_week_start, prev_week_end))
299+
.filter(events::Column::SessionId.is_not_null())
300+
.select_only()
301+
.column(events::Column::SessionId)
302+
.distinct()
303+
.count(self.db.as_ref())
304+
.await? as i64;
305+
306+
let week_over_week_change = if prev_visitors > 0 {
307+
((visitors - prev_visitors) as f64 / prev_visitors as f64) * 100.0
308+
} else if visitors > 0 {
309+
100.0 // If we had 0 before and now have some, that's 100% increase
310+
} else {
311+
0.0
312+
};
277313

278-
let mut audit_log_summary: HashMap<String, i64> = HashMap::new();
279-
for log in audit_logs_list {
280-
*audit_log_summary.entry(log.operation_type).or_insert(0) += 1;
314+
// Only include projects that have activity
315+
if visitors > 0 || page_views > 0 || deployment_count > 0 {
316+
project_stats.push(ProjectStats {
317+
project_id: project.id,
318+
project_name: project.name.clone(),
319+
project_slug: project.slug.clone(),
320+
visitors,
321+
page_views,
322+
unique_sessions: visitors, // Same as visitors (unique sessions)
323+
deployments: deployment_count,
324+
week_over_week_change,
325+
});
326+
}
281327
}
282328

283-
Ok(SecurityData {
284-
new_user_signups,
285-
git_provider_connections: 0,
286-
api_key_usage: 0,
287-
suspicious_activities: vec![],
288-
audit_log_summary,
289-
})
290-
}
329+
// Sort projects by visitors (most active first)
330+
project_stats.sort_by(|a, b| b.visitors.cmp(&a.visitors));
291331

292-
/// Aggregate resource and cost data
293-
async fn aggregate_resource_data(
294-
&self,
295-
__week_start: DateTime<Utc>,
296-
__week_end: DateTime<Utc>,
297-
) -> Result<ResourceData> {
298-
// TODO: Implement resource usage queries (S3, database size, etc.)
299-
Ok(ResourceData {
300-
storage_used_mb: 0.0,
301-
storage_growth_mb: 0.0,
302-
database_size_mb: 0.0,
303-
database_growth_mb: 0.0,
304-
log_volume_mb: 0.0,
305-
backup_count: 0,
306-
backup_size_mb: 0.0,
307-
recommendations: vec![],
308-
})
332+
Ok(project_stats)
309333
}
310334

311335
/// Build executive summary from aggregated data
@@ -482,19 +506,18 @@ mod tests {
482506
}
483507

484508
#[tokio::test]
485-
async fn test_aggregate_security_data_empty() {
509+
async fn test_aggregate_project_data_empty() {
486510
let (service, test_db) = setup_test_service().await;
487511

488512
let now = Utc::now();
489513
let week_start = now - Duration::days(7);
490514

491-
let security = service
492-
.aggregate_security_data(week_start, now)
515+
let projects = service
516+
.aggregate_project_data(week_start, now)
493517
.await
494-
.expect("Failed to aggregate security data");
518+
.expect("Failed to aggregate project data");
495519

496-
assert_eq!(security.new_user_signups, 0);
497-
assert!(security.audit_log_summary.is_empty());
520+
assert_eq!(projects.len(), 0);
498521

499522
test_db.cleanup_all_tables().await.expect("Cleanup failed");
500523
}
@@ -758,31 +781,82 @@ mod tests {
758781
}
759782

760783
#[tokio::test]
761-
async fn test_aggregate_security_with_real_users() {
784+
async fn test_aggregate_project_data_with_activity() {
762785
let (service, test_db) = setup_test_service().await;
763786

764787
let now = Utc::now();
765788
let week_start = now - Duration::days(7);
766789

767-
// Create test users in current week
768-
for i in 0..3 {
769-
let user = users::ActiveModel {
770-
name: Set(format!("User {}", i)),
771-
email: Set(format!("user{}@example.com", i)),
772-
password_hash: Set(Some("hash".to_string())),
773-
created_at: Set(now - Duration::hours(i as i64)),
774-
updated_at: Set(now),
790+
// Create test project
791+
let project = projects::ActiveModel {
792+
name: Set("test-project".to_string()),
793+
slug: Set("test-project".to_string()),
794+
repo_name: Set("test-repo".to_string()),
795+
repo_owner: Set("test-owner".to_string()),
796+
directory: Set("/".to_string()),
797+
main_branch: Set("main".to_string()),
798+
preset: Set(temps_entities::preset::Preset::Astro),
799+
created_at: Set(now),
800+
updated_at: Set(now),
801+
..Default::default()
802+
};
803+
let project = project.insert(test_db.connection()).await.unwrap();
804+
805+
// Create test environment
806+
let environment = environments::ActiveModel {
807+
project_id: Set(project.id),
808+
name: Set("production".to_string()),
809+
slug: Set("production".to_string()),
810+
subdomain: Set("production".to_string()),
811+
host: Set("production.example.com".to_string()),
812+
upstreams: Set(temps_entities::upstream_config::UpstreamList::default()),
813+
created_at: Set(now),
814+
updated_at: Set(now),
815+
..Default::default()
816+
};
817+
let environment = environment.insert(test_db.connection()).await.unwrap();
818+
819+
// Create test deployment
820+
let deployment = deployments::ActiveModel {
821+
project_id: Set(project.id),
822+
environment_id: Set(environment.id),
823+
slug: Set("deploy-1".to_string()),
824+
state: Set("completed".to_string()),
825+
metadata: Set(Some(deployments::DeploymentMetadata::default())),
826+
commit_sha: Set(Some("abc123".to_string())),
827+
branch_ref: Set(Some("refs/heads/main".to_string())),
828+
created_at: Set(now),
829+
updated_at: Set(now),
830+
..Default::default()
831+
};
832+
let deployment = deployment.insert(test_db.connection()).await.unwrap();
833+
834+
// Create test events (simulating visitors and page views)
835+
for i in 0..5 {
836+
let event = events::ActiveModel {
837+
project_id: Set(project.id),
838+
environment_id: Set(Some(environment.id)),
839+
deployment_id: Set(Some(deployment.id)),
840+
session_id: Set(Some(format!("session-{}", i))),
841+
event_type: Set("pageview".to_string()),
842+
timestamp: Set(now - Duration::hours(i as i64)),
843+
hostname: Set("example.com".to_string()),
844+
pathname: Set("/".to_string()),
845+
page_path: Set("/".to_string()),
846+
href: Set("https://example.com/".to_string()),
775847
..Default::default()
776848
};
777-
user.insert(test_db.connection()).await.unwrap();
849+
event.insert(test_db.connection()).await.unwrap();
778850
}
779851

780-
let security = service
781-
.aggregate_security_data(week_start, now)
852+
let projects_data = service
853+
.aggregate_project_data(week_start, now)
782854
.await
783-
.expect("Failed to aggregate security data");
855+
.expect("Failed to aggregate project data");
784856

785-
assert_eq!(security.new_user_signups, 3);
857+
assert_eq!(projects_data.len(), 1);
858+
assert_eq!(projects_data[0].project_name, "test-project");
859+
assert!(projects_data[0].visitors > 0);
786860

787861
test_db.cleanup_all_tables().await.expect("Cleanup failed");
788862
}
@@ -899,7 +973,7 @@ mod tests {
899973
assert!(digest.has_data());
900974
assert!(digest.performance.is_some());
901975
assert!(digest.deployments.is_some());
902-
assert!(digest.security.is_some());
976+
assert!(!digest.projects.is_empty());
903977

904978
// Verify performance data
905979
let perf = digest.performance.unwrap();
@@ -911,9 +985,9 @@ mod tests {
911985
assert_eq!(deploy.successful_deployments, 5); // 1 initial + 4 from loop
912986
assert_eq!(deploy.failed_deployments, 1);
913987

914-
// Verify security data
915-
let security = digest.security.unwrap();
916-
assert_eq!(security.new_user_signups, 2);
988+
// Verify project data
989+
assert_eq!(digest.projects.len(), 1);
990+
assert_eq!(digest.projects[0].project_name, "integration-test-project");
917991

918992
// Verify executive summary
919993
assert_eq!(digest.executive_summary.total_visitors, 10);

0 commit comments

Comments
 (0)