Repository: parseablehq/parseable
Pull request #1224: Updates for Prism
Author: login: parmesant
Created: 2025-03-05T11:16:33Z | Merged: 2025-03-06T03:32:13Z

- Modified dashboards and filters to have async safe RwLock
- Modified dashboards and filters to send back list of all available items (any user can list all items)
- Bugfix for stats
- Modified stats format to be sent back as u64 instead of `format!("{number}  Bytes")`
- `fetch_daily_stats_from_ingestors()` function will read `stream.json` files for each ingestor and calculate date-wise stats instead of calling all ingestors' /metric endpoint

<!-- Thanks for trying to help us make Parseable be the best it can be! Please fill out as much of the following information as is possible (where relevant, and remove it when irrelevant) to help make the intention and scope of this PR clear in order to ease review. -->

Fixes #XXXX.

<!-- Replace XXXX with the id of the issue fixed in this PR. Remove this section if there is no corresponding issue. Don't reference the issue in the title of this pull-request. -->

### Description

<!-- Describe the goal of this PR -->

<!-- Describe the possible solutions and chosen one with the rationale. -->

<!-- Describe key changes made in the patch. -->

<hr>

This PR has:
- [ ] been tested to ensure log ingestion and log query works.
- [ ] added comments explaining the "why" and the intent of the code wherever would not be obvious for an unfamiliar reader.
- [ ] added documentation for new or modified features or behaviors.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Improved aggregation of stream usage statistics for more accurate metric insights.
  - Enhanced error messaging for dashboards and filters, offering clearer feedback.

- **Bug Fixes**
  - Resolved issues with user ID handling in dashboard and filter operations.

- **Refactor**
  - Transitioned key operations to asynchronous processing, resulting in smoother and more responsive user interactions.
  - Updated display of size metrics to show raw numeric values.
  - Streamlined session handling for dashboards and filters by leveraging improved session key management.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/handlers/http/cluster/mod.rs b/src/handlers/http/cluster/mod.rs
index 60a61a1ec..3232d54cb 100644
--- a/src/handlers/http/cluster/mod.rs
+++ b/src/handlers/http/cluster/mod.rs
@@ -367,64 +367,29 @@ pub async fn sync_role_update_with_ingestors(
     Ok(())
 }
 
-pub async fn fetch_daily_stats_from_ingestors(
-    stream_name: &str,
+pub fn fetch_daily_stats_from_ingestors(
     date: &str,
+    stream_meta_list: &[ObjectStoreFormat],
 ) -> Result<Stats, StreamError> {
-    let mut total_events_ingested: u64 = 0;
-    let mut total_ingestion_size: u64 = 0;
-    let mut total_storage_size: u64 = 0;
-
-    let ingestor_infos = get_ingestor_info().await.map_err(|err| {
-        error!("Fatal: failed to get ingestor info: {:?}", err);
-        StreamError::Anyhow(err)
-    })?;
-    for ingestor in ingestor_infos.iter() {
-        let uri = Url::parse(&format!(
-            "{}{}/metrics",
-            &ingestor.domain_name,
-            base_path_without_preceding_slash()
-        ))
-        .map_err(|err| {
-            StreamError::Anyhow(anyhow::anyhow!("Invalid URL in Ingestor Metadata: {}", err))
-        })?;
-
-        let res = HTTP_CLIENT
-            .get(uri)
-            .header(header::AUTHORIZATION, &ingestor.token)
-            .header(header::CONTENT_TYPE, "application/json")
-            .send()
-            .await;
-
-        if let Ok(res) = res {
-            let text = res
-                .text()
-                .await
-                .map_err(|err| StreamError::Anyhow(anyhow::anyhow!("Request failed: {}", err)))?;
-            let lines: Vec<Result<String, std::io::Error>> =
-                text.lines().map(|line| Ok(line.to_owned())).collect_vec();
-
-            let sample = prometheus_parse::Scrape::parse(lines.into_iter())
-                .map_err(|err| {
-                    StreamError::Anyhow(anyhow::anyhow!(
-                        "Invalid URL in Ingestor Metadata: {}",
-                        err
-                    ))
-                })?
-                .samples;
-
-            let (events_ingested, ingestion_size, storage_size) =
-                Metrics::get_daily_stats_from_samples(sample, stream_name, date);
-            total_events_ingested += events_ingested;
-            total_ingestion_size += ingestion_size;
-            total_storage_size += storage_size;
+    // for the given date, get the stats from the ingestors
+    let mut events_ingested = 0;
+    let mut ingestion_size = 0;
+    let mut storage_size = 0;
+
+    for meta in stream_meta_list.iter() {
+        for manifest in meta.snapshot.manifest_list.iter() {
+            if manifest.time_lower_bound.date_naive().to_string() == date {
+                events_ingested += manifest.events_ingested;
+                ingestion_size += manifest.ingestion_size;
+                storage_size += manifest.storage_size;
+            }
         }
     }
 
     let stats = Stats {
-        events: total_events_ingested,
-        ingestion: total_ingestion_size,
-        storage: total_storage_size,
+        events: events_ingested,
+        ingestion: ingestion_size,
+        storage: storage_size,
     };
     Ok(stats)
 }
@@ -474,17 +439,17 @@ pub async fn fetch_stats_from_ingestors(
         Utc::now(),
         IngestionStats::new(
             count,
-            format!("{} Bytes", ingestion_size),
+            ingestion_size,
             lifetime_count,
-            format!("{} Bytes", lifetime_ingestion_size),
+            lifetime_ingestion_size,
             deleted_count,
-            format!("{} Bytes", deleted_ingestion_size),
+            deleted_ingestion_size,
             "json",
         ),
         StorageStats::new(
-            format!("{} Bytes", storage_size),
-            format!("{} Bytes", lifetime_storage_size),
-            format!("{} Bytes", deleted_storage_size),
+            storage_size,
+            lifetime_storage_size,
+            deleted_storage_size,
             "parquet",
         ),
     );
diff --git a/src/handlers/http/cluster/utils.rs b/src/handlers/http/cluster/utils.rs
index b41582d70..b88e95bb1 100644
--- a/src/handlers/http/cluster/utils.rs
+++ b/src/handlers/http/cluster/utils.rs
@@ -19,7 +19,6 @@
 use crate::{handlers::http::base_path_without_preceding_slash, HTTP_CLIENT};
 use actix_web::http::header;
 use chrono::{DateTime, Utc};
-use itertools::Itertools;
 use serde::{Deserialize, Serialize};
 use tracing::error;
 use url::Url;
@@ -81,22 +80,22 @@ impl ClusterInfo {
 #[derive(Debug, Default, Serialize, Deserialize)]
 pub struct IngestionStats {
     pub count: u64,
-    pub size: String,
+    pub size: u64,
     pub format: String,
     pub lifetime_count: u64,
-    pub lifetime_size: String,
+    pub lifetime_size: u64,
     pub deleted_count: u64,
-    pub deleted_size: String,
+    pub deleted_size: u64,
 }
 
 impl IngestionStats {
     pub fn new(
         count: u64,
-        size: String,
+        size: u64,
         lifetime_count: u64,
-        lifetime_size: String,
+        lifetime_size: u64,
         deleted_count: u64,
-        deleted_size: String,
+        deleted_size: u64,
         format: &str,
     ) -> Self {
         Self {
@@ -113,14 +112,14 @@ impl IngestionStats {
 
 #[derive(Debug, Default, Serialize, Deserialize)]
 pub struct StorageStats {
-    pub size: String,
+    pub size: u64,
     pub format: String,
-    pub lifetime_size: String,
-    pub deleted_size: String,
+    pub lifetime_size: u64,
+    pub deleted_size: u64,
 }
 
 impl StorageStats {
-    pub fn new(size: String, lifetime_size: String, deleted_size: String, format: &str) -> Self {
+    pub fn new(size: u64, lifetime_size: u64, deleted_size: u64, format: &str) -> Self {
         Self {
             size,
             format: format.to_string(),
@@ -143,36 +142,12 @@ pub fn merge_quried_stats(stats: Vec<QueriedStats>) -> QueriedStats {
             .fold(IngestionStats::default(), |acc, x| IngestionStats {
                 count: acc.count + x.count,
 
-                size: format!(
-                    "{} Bytes",
-                    acc.size.split(' ').collect_vec()[0]
-                        .parse::<u64>()
-                        .unwrap_or_default()
-                        + x.size.split(' ').collect_vec()[0]
-                            .parse::<u64>()
-                            .unwrap_or_default()
-                ),
+                size: acc.size + x.size,
                 format: x.format.clone(),
                 lifetime_count: acc.lifetime_count + x.lifetime_count,
-                lifetime_size: format!(
-                    "{} Bytes",
-                    acc.lifetime_size.split(' ').collect_vec()[0]
-                        .parse::<u64>()
-                        .unwrap_or_default()
-                        + x.lifetime_size.split(' ').collect_vec()[0]
-                            .parse::<u64>()
-                            .unwrap_or_default()
-                ),
+                lifetime_size: acc.lifetime_size + x.lifetime_size,
                 deleted_count: acc.deleted_count + x.deleted_count,
-                deleted_size: format!(
-                    "{} Bytes",
-                    acc.deleted_size.split(' ').collect_vec()[0]
-                        .parse::<u64>()
-                        .unwrap_or_default()
-                        + x.deleted_size.split(' ').collect_vec()[0]
-                            .parse::<u64>()
-                            .unwrap_or_default()
-                ),
+                deleted_size: acc.deleted_size + x.deleted_size,
             });
 
     let cumulative_storage =
@@ -180,34 +155,10 @@ pub fn merge_quried_stats(stats: Vec<QueriedStats>) -> QueriedStats {
             .iter()
             .map(|x| &x.storage)
             .fold(StorageStats::default(), |acc, x| StorageStats {
-                size: format!(
-                    "{} Bytes",
-                    acc.size.split(' ').collect_vec()[0]
-                        .parse::<u64>()
-                        .unwrap_or_default()
-                        + x.size.split(' ').collect_vec()[0]
-                            .parse::<u64>()
-                            .unwrap_or_default()
-                ),
+                size: acc.size + x.size,
                 format: x.format.clone(),
-                lifetime_size: format!(
-                    "{} Bytes",
-                    acc.lifetime_size.split(' ').collect_vec()[0]
-                        .parse::<u64>()
-                        .unwrap_or_default()
-                        + x.lifetime_size.split(' ').collect_vec()[0]
-                            .parse::<u64>()
-                            .unwrap_or_default()
-                ),
-                deleted_size: format!(
-                    "{} Bytes",
-                    acc.deleted_size.split(' ').collect_vec()[0]
-                        .parse::<u64>()
-                        .unwrap_or_default()
-                        + x.deleted_size.split(' ').collect_vec()[0]
-                            .parse::<u64>()
-                            .unwrap_or_default()
-                ),
+                lifetime_size: acc.lifetime_size + x.lifetime_size,
+                deleted_size: acc.deleted_size + x.deleted_size,
             });
 
     QueriedStats::new(
diff --git a/src/handlers/http/logstream.rs b/src/handlers/http/logstream.rs
index 107c706ed..e6b9dff10 100644
--- a/src/handlers/http/logstream.rs
+++ b/src/handlers/http/logstream.rs
@@ -262,17 +262,17 @@ pub async fn get_stats(
     let stats = {
         let ingestion_stats = IngestionStats::new(
             stats.current_stats.events,
-            format!("{} Bytes", stats.current_stats.ingestion),
+            stats.current_stats.ingestion,
             stats.lifetime_stats.events,
-            format!("{} Bytes", stats.lifetime_stats.ingestion),
+            stats.lifetime_stats.ingestion,
             stats.deleted_stats.events,
-            format!("{} Bytes", stats.deleted_stats.ingestion),
+            stats.deleted_stats.ingestion,
             "json",
         );
         let storage_stats = StorageStats::new(
-            format!("{} Bytes", stats.current_stats.storage),
-            format!("{} Bytes", stats.lifetime_stats.storage),
-            format!("{} Bytes", stats.deleted_stats.storage),
+            stats.current_stats.storage,
+            stats.lifetime_stats.storage,
+            stats.deleted_stats.storage,
             "parquet",
         );
 
diff --git a/src/handlers/http/modal/query/querier_logstream.rs b/src/handlers/http/modal/query/querier_logstream.rs
index 740855bb7..76d282f76 100644
--- a/src/handlers/http/modal/query/querier_logstream.rs
+++ b/src/handlers/http/modal/query/querier_logstream.rs
@@ -26,6 +26,7 @@ use actix_web::{
 use bytes::Bytes;
 use chrono::Utc;
 use http::StatusCode;
+use relative_path::RelativePathBuf;
 use tokio::sync::Mutex;
 use tracing::{error, warn};
 
@@ -44,7 +45,7 @@ use crate::{
     hottier::HotTierManager,
     parseable::{StreamNotFound, PARSEABLE},
     stats::{self, Stats},
-    storage::StreamType,
+    storage::{ObjectStoreFormat, StreamType, STREAM_ROOT_DIRECTORY},
 };
 
 pub async fn delete(stream_name: Path<String>) -> Result<impl Responder, StreamError> {
@@ -151,7 +152,35 @@ pub async fn get_stats(
 
         if !date_value.is_empty() {
             let querier_stats = get_stats_date(&stream_name, date_value).await?;
-            let ingestor_stats = fetch_daily_stats_from_ingestors(&stream_name, date_value).await?;
+
+            // this function requires all the ingestor stream jsons
+            let path = RelativePathBuf::from_iter([&stream_name, STREAM_ROOT_DIRECTORY]);
+            let obs = PARSEABLE
+                .storage
+                .get_object_store()
+                .get_objects(
+                    Some(&path),
+                    Box::new(|file_name| {
+                        file_name.starts_with(".ingestor") && file_name.ends_with("stream.json")
+                    }),
+                )
+                .await?;
+
+            let mut ingestor_stream_jsons = Vec::new();
+            for ob in obs {
+                let stream_metadata: ObjectStoreFormat = match serde_json::from_slice(&ob) {
+                    Ok(d) => d,
+                    Err(e) => {
+                        error!("Failed to parse stream metadata: {:?}", e);
+                        continue;
+                    }
+                };
+                ingestor_stream_jsons.push(stream_metadata);
+            }
+
+            let ingestor_stats =
+                fetch_daily_stats_from_ingestors(date_value, &ingestor_stream_jsons)?;
+
             let total_stats = Stats {
                 events: querier_stats.events + ingestor_stats.events,
                 ingestion: querier_stats.ingestion + ingestor_stats.ingestion,
@@ -180,17 +209,17 @@ pub async fn get_stats(
     let stats = {
         let ingestion_stats = IngestionStats::new(
             stats.current_stats.events,
-            format!("{} Bytes", stats.current_stats.ingestion),
+            stats.current_stats.ingestion,
             stats.lifetime_stats.events,
-            format!("{} Bytes", stats.lifetime_stats.ingestion),
+            stats.lifetime_stats.ingestion,
             stats.deleted_stats.events,
-            format!("{} Bytes", stats.deleted_stats.ingestion),
+            stats.deleted_stats.ingestion,
             "json",
         );
         let storage_stats = StorageStats::new(
-            format!("{} Bytes", stats.current_stats.storage),
-            format!("{} Bytes", stats.lifetime_stats.storage),
-            format!("{} Bytes", stats.deleted_stats.storage),
+            stats.current_stats.storage,
+            stats.lifetime_stats.storage,
+            stats.deleted_stats.storage,
             "parquet",
         );
 
diff --git a/src/handlers/http/users/dashboards.rs b/src/handlers/http/users/dashboards.rs
index 32619f0f0..16a885969 100644
--- a/src/handlers/http/users/dashboards.rs
+++ b/src/handlers/http/users/dashboards.rs
@@ -21,7 +21,7 @@ use crate::{
     parseable::PARSEABLE,
     storage::{object_storage::dashboard_path, ObjectStorageError},
     users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS},
-    utils::{get_hash, get_user_from_request},
+    utils::{actix::extract_session_key_from_req, get_hash, get_user_from_request},
 };
 use actix_web::{
     http::header::ContentType,
@@ -36,8 +36,9 @@ use http::StatusCode;
 use serde_json::Error as SerdeError;
 
 pub async fn list(req: HttpRequest) -> Result<impl Responder, DashboardError> {
-    let user_id = get_user_from_request(&req)?;
-    let dashboards = DASHBOARDS.list_dashboards_by_user(&get_hash(&user_id));
+    let key =
+        extract_session_key_from_req(&req).map_err(|e| DashboardError::Custom(e.to_string()))?;
+    let dashboards = DASHBOARDS.list_dashboards(&key).await;
 
     Ok((web::Json(dashboards), StatusCode::OK))
 }
@@ -49,7 +50,10 @@ pub async fn get(
     let user_id = get_user_from_request(&req)?;
     let dashboard_id = dashboard_id.into_inner();
 
-    if let Some(dashboard) = DASHBOARDS.get_dashboard(&dashboard_id, &get_hash(&user_id)) {
+    if let Some(dashboard) = DASHBOARDS
+        .get_dashboard(&dashboard_id, &get_hash(&user_id))
+        .await
+    {
         return Ok((web::Json(dashboard), StatusCode::OK));
     }
 
@@ -77,7 +81,7 @@ pub async fn post(
             .as_str(),
         ));
     }
-    DASHBOARDS.update(&dashboard);
+    DASHBOARDS.update(&dashboard).await;
 
     let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id));
 
@@ -99,7 +103,11 @@ pub async fn update(
     user_id = get_hash(&user_id);
     let dashboard_id = dashboard_id.into_inner();
 
-    if DASHBOARDS.get_dashboard(&dashboard_id, &user_id).is_none() {
+    if DASHBOARDS
+        .get_dashboard(&dashboard_id, &user_id)
+        .await
+        .is_none()
+    {
         return Err(DashboardError::Metadata("Dashboard does not exist"));
     }
     dashboard.dashboard_id = Some(dashboard_id.to_string());
@@ -110,7 +118,7 @@ pub async fn update(
             tile.tile_id = Some(get_hash(Utc::now().timestamp_micros().to_string().as_str()));
         }
     }
-    DASHBOARDS.update(&dashboard);
+    DASHBOARDS.update(&dashboard).await;
 
     let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id));
 
@@ -130,14 +138,18 @@ pub async fn delete(
     let mut user_id = get_user_from_request(&req)?;
     user_id = get_hash(&user_id);
     let dashboard_id = dashboard_id.into_inner();
-    if DASHBOARDS.get_dashboard(&dashboard_id, &user_id).is_none() {
+    if DASHBOARDS
+        .get_dashboard(&dashboard_id, &user_id)
+        .await
+        .is_none()
+    {
         return Err(DashboardError::Metadata("Dashboard does not exist"));
     }
     let path = dashboard_path(&user_id, &format!("{}.json", dashboard_id));
     let store = PARSEABLE.storage.get_object_store();
     store.delete_object(&path).await?;
 
-    DASHBOARDS.delete_dashboard(&dashboard_id);
+    DASHBOARDS.delete_dashboard(&dashboard_id).await;
 
     Ok(HttpResponse::Ok().finish())
 }
@@ -152,6 +164,8 @@ pub enum DashboardError {
     Metadata(&'static str),
     #[error("User does not exist")]
     UserDoesNotExist(#[from] RBACError),
+    #[error("Error: {0}")]
+    Custom(String),
 }
 
 impl actix_web::ResponseError for DashboardError {
@@ -161,6 +175,7 @@ impl actix_web::ResponseError for DashboardError {
             Self::Serde(_) => StatusCode::BAD_REQUEST,
             Self::Metadata(_) => StatusCode::BAD_REQUEST,
             Self::UserDoesNotExist(_) => StatusCode::NOT_FOUND,
+            Self::Custom(_) => StatusCode::INTERNAL_SERVER_ERROR,
         }
     }
 
diff --git a/src/handlers/http/users/filters.rs b/src/handlers/http/users/filters.rs
index 7ea3c8a09..378e80df3 100644
--- a/src/handlers/http/users/filters.rs
+++ b/src/handlers/http/users/filters.rs
@@ -21,7 +21,7 @@ use crate::{
     parseable::PARSEABLE,
     storage::{object_storage::filter_path, ObjectStorageError},
     users::filters::{Filter, CURRENT_FILTER_VERSION, FILTERS},
-    utils::{get_hash, get_user_from_request},
+    utils::{actix::extract_session_key_from_req, get_hash, get_user_from_request},
 };
 use actix_web::{
     http::header::ContentType,
@@ -34,8 +34,9 @@ use http::StatusCode;
 use serde_json::Error as SerdeError;
 
 pub async fn list(req: HttpRequest) -> Result<impl Responder, FiltersError> {
-    let user_id = get_user_from_request(&req)?;
-    let filters = FILTERS.list_filters_by_user(&get_hash(&user_id));
+    let key =
+        extract_session_key_from_req(&req).map_err(|e| FiltersError::Custom(e.to_string()))?;
+    let filters = FILTERS.list_filters(&key).await;
     Ok((web::Json(filters), StatusCode::OK))
 }
 
@@ -46,7 +47,7 @@ pub async fn get(
     let user_id = get_user_from_request(&req)?;
     let filter_id = filter_id.into_inner();
 
-    if let Some(filter) = FILTERS.get_filter(&filter_id, &get_hash(&user_id)) {
+    if let Some(filter) = FILTERS.get_filter(&filter_id, &get_hash(&user_id)).await {
         return Ok((web::Json(filter), StatusCode::OK));
     }
 
@@ -63,7 +64,7 @@ pub async fn post(
     filter.filter_id = Some(filter_id.clone());
     filter.user_id = Some(user_id.clone());
     filter.version = Some(CURRENT_FILTER_VERSION.to_string());
-    FILTERS.update(&filter);
+    FILTERS.update(&filter).await;
 
     let path = filter_path(
         &user_id,
@@ -86,13 +87,13 @@ pub async fn update(
     let mut user_id = get_user_from_request(&req)?;
     user_id = get_hash(&user_id);
     let filter_id = filter_id.into_inner();
-    if FILTERS.get_filter(&filter_id, &user_id).is_none() {
+    if FILTERS.get_filter(&filter_id, &user_id).await.is_none() {
         return Err(FiltersError::Metadata("Filter does not exist"));
     }
     filter.filter_id = Some(filter_id.clone());
     filter.user_id = Some(user_id.clone());
     filter.version = Some(CURRENT_FILTER_VERSION.to_string());
-    FILTERS.update(&filter);
+    FILTERS.update(&filter).await;
 
     let path = filter_path(
         &user_id,
@@ -116,6 +117,7 @@ pub async fn delete(
     let filter_id = filter_id.into_inner();
     let filter = FILTERS
         .get_filter(&filter_id, &user_id)
+        .await
         .ok_or(FiltersError::Metadata("Filter does not exist"))?;
 
     let path = filter_path(
@@ -126,7 +128,7 @@ pub async fn delete(
     let store = PARSEABLE.storage.get_object_store();
     store.delete_object(&path).await?;
 
-    FILTERS.delete_filter(&filter_id);
+    FILTERS.delete_filter(&filter_id).await;
 
     Ok(HttpResponse::Ok().finish())
 }
@@ -141,6 +143,8 @@ pub enum FiltersError {
     Metadata(&'static str),
     #[error("User does not exist")]
     UserDoesNotExist(#[from] RBACError),
+    #[error("Error: {0}")]
+    Custom(String),
 }
 
 impl actix_web::ResponseError for FiltersError {
@@ -150,6 +154,7 @@ impl actix_web::ResponseError for FiltersError {
             Self::Serde(_) => StatusCode::BAD_REQUEST,
             Self::Metadata(_) => StatusCode::BAD_REQUEST,
             Self::UserDoesNotExist(_) => StatusCode::NOT_FOUND,
+            Self::Custom(_) => StatusCode::INTERNAL_SERVER_ERROR,
         }
     }
 
diff --git a/src/prism/home/mod.rs b/src/prism/home/mod.rs
index 96c55d43d..48b062e31 100644
--- a/src/prism/home/mod.rs
+++ b/src/prism/home/mod.rs
@@ -16,19 +16,27 @@
  *
  */
 
+use std::collections::HashMap;
+
 use actix_web::http::header::ContentType;
 use chrono::Utc;
 use http::StatusCode;
 use itertools::Itertools;
+use relative_path::RelativePathBuf;
 use serde::Serialize;
+use tracing::error;
 
 use crate::{
     alerts::{get_alerts_info, AlertError, AlertsInfo, ALERTS},
     correlation::{CorrelationError, CORRELATIONS},
-    handlers::http::logstream::{error::StreamError, get_stats_date},
+    handlers::http::{
+        cluster::fetch_daily_stats_from_ingestors,
+        logstream::{error::StreamError, get_stats_date},
+    },
     parseable::PARSEABLE,
     rbac::{map::SessionKey, role::Action, Users},
     stats::Stats,
+    storage::{ObjectStorageError, ObjectStoreFormat, STREAM_ROOT_DIRECTORY},
     users::{dashboards::DASHBOARDS, filters::FILTERS},
 };
 
@@ -67,14 +75,6 @@ pub struct HomeResponse {
 }
 
 pub async fn generate_home_response(key: &SessionKey) -> Result<HomeResponse, PrismHomeError> {
-    let user_id = if let Some(user_id) = Users.get_username_from_session(key) {
-        user_id
-    } else {
-        return Err(PrismHomeError::Anyhow(anyhow::Error::msg(
-            "User does not exist",
-        )));
-    };
-
     // get all stream titles
     let stream_titles: Vec<String> = PARSEABLE
         .storage
@@ -115,7 +115,8 @@ pub async fn generate_home_response(key: &SessionKey) -> Result<HomeResponse, Pr
 
     // get dashboard IDs
     let dashboard_titles = DASHBOARDS
-        .list_dashboards_by_user(&user_id)
+        .list_dashboards(key)
+        .await
         .iter()
         .map(|dashboard| TitleAndId {
             title: dashboard.name.clone(),
@@ -130,7 +131,8 @@ pub async fn generate_home_response(key: &SessionKey) -> Result<HomeResponse, Pr
 
     // get filter IDs
     let filter_titles = FILTERS
-        .list_filters_by_user(&user_id)
+        .list_filters(key)
+        .await
         .iter()
         .map(|filter| TitleAndId {
             title: filter.filter_name.clone(),
@@ -159,35 +161,50 @@ pub async fn generate_home_response(key: &SessionKey) -> Result<HomeResponse, Pr
 
     let mut stream_details = Vec::new();
 
+    // this will hold the summary of all streams for the last 7 days
     let mut summary = StreamInfo::default();
 
-    for date in dates.iter() {
-        let mut details = DatedStats {
-            date: date.clone(),
-            ..Default::default()
-        };
-
-        for stream in stream_titles.iter() {
-            let stats = get_stats_date(stream, date).await?;
+    let mut stream_wise_ingestor_stream_json = HashMap::new();
+    for stream in stream_titles.clone() {
+        let path = RelativePathBuf::from_iter([&stream, STREAM_ROOT_DIRECTORY]);
+        let obs = PARSEABLE
+            .storage
+            .get_object_store()
+            .get_objects(
+                Some(&path),
+                Box::new(|file_name| {
+                    file_name.starts_with(".ingestor") && file_name.ends_with("stream.json")
+                }),
+            )
+            .await?;
 
-            // collect date-wise stats for all streams
-            details.events += stats.events;
-            details.ingestion_size += stats.ingestion;
-            details.storage_size += stats.storage;
-
-            // collect all 7-day stats for all streams
-            summary.stats_summary.events += stats.events;
-            summary.stats_summary.ingestion += stats.ingestion;
-            summary.stats_summary.storage += stats.storage;
+        let mut ingestor_stream_jsons = Vec::new();
+        for ob in obs {
+            let stream_metadata: ObjectStoreFormat = match serde_json::from_slice(&ob) {
+                Ok(d) => d,
+                Err(e) => {
+                    error!("Failed to parse stream metadata: {:?}", e);
+                    continue;
+                }
+            };
+            ingestor_stream_jsons.push(stream_metadata);
         }
+        stream_wise_ingestor_stream_json.insert(stream, ingestor_stream_jsons);
+    }
 
-        stream_details.push(details);
+    for date in dates.into_iter() {
+        let dated_stats = stats_for_date(date, stream_wise_ingestor_stream_json.clone()).await?;
+        summary.stats_summary.events += dated_stats.events;
+        summary.stats_summary.ingestion += dated_stats.ingestion_size;
+        summary.stats_summary.storage += dated_stats.storage_size;
+
+        stream_details.push(dated_stats);
     }
 
     Ok(HomeResponse {
         stream_info: summary,
         stats_details: stream_details,
-        stream_titles,
+        stream_titles: stream_titles.clone(),
         alert_titles,
         correlation_titles,
         dashboard_titles,
@@ -196,6 +213,28 @@ pub async fn generate_home_response(key: &SessionKey) -> Result<HomeResponse, Pr
     })
 }
 
+async fn stats_for_date(
+    date: String,
+    stream_wise_meta: HashMap<String, Vec<ObjectStoreFormat>>,
+) -> Result<DatedStats, PrismHomeError> {
+    // collect stats for all the streams for the given date
+    let mut details = DatedStats {
+        date: date.clone(),
+        ..Default::default()
+    };
+
+    for (stream, meta) in stream_wise_meta {
+        let querier_stats = get_stats_date(&stream, &date).await?;
+        let ingestor_stats = fetch_daily_stats_from_ingestors(&date, &meta)?;
+        // collect date-wise stats for all streams
+        details.events += querier_stats.events + ingestor_stats.events;
+        details.ingestion_size += querier_stats.ingestion + ingestor_stats.ingestion;
+        details.storage_size += querier_stats.storage + ingestor_stats.storage;
+    }
+
+    Ok(details)
+}
+
 #[derive(Debug, thiserror::Error)]
 pub enum PrismHomeError {
     #[error("Error: {0}")]
@@ -206,6 +245,8 @@ pub enum PrismHomeError {
     CorrelationError(#[from] CorrelationError),
     #[error("StreamError: {0}")]
     StreamError(#[from] StreamError),
+    #[error("ObjectStorageError: {0}")]
+    ObjectStorageError(#[from] ObjectStorageError),
 }
 
 impl actix_web::ResponseError for PrismHomeError {
@@ -215,6 +256,7 @@ impl actix_web::ResponseError for PrismHomeError {
             PrismHomeError::AlertError(e) => e.status_code(),
             PrismHomeError::CorrelationError(e) => e.status_code(),
             PrismHomeError::StreamError(e) => e.status_code(),
+            PrismHomeError::ObjectStorageError(_) => StatusCode::INTERNAL_SERVER_ERROR,
         }
     }
 
diff --git a/src/prism/logstream/mod.rs b/src/prism/logstream/mod.rs
index ebdd463ec..e9ffcb981 100644
--- a/src/prism/logstream/mod.rs
+++ b/src/prism/logstream/mod.rs
@@ -26,13 +26,16 @@ use serde::Serialize;
 
 use crate::{
     handlers::http::{
-        cluster::utils::{merge_quried_stats, IngestionStats, QueriedStats, StorageStats},
+        cluster::{
+            fetch_stats_from_ingestors,
+            utils::{merge_quried_stats, IngestionStats, QueriedStats, StorageStats},
+        },
         logstream::error::StreamError,
         query::update_schema_when_distributed,
     },
     parseable::{StreamNotFound, PARSEABLE},
     stats,
-    storage::{retention::Retention, StreamInfo},
+    storage::{retention::Retention, StreamInfo, StreamType},
     LOCK_EXPECT,
 };
 
@@ -93,24 +96,31 @@ async fn get_stats(stream_name: &str) -> Result<QueriedStats, PrismLogstreamErro
     let stats = stats::get_current_stats(stream_name, "json")
         .ok_or_else(|| StreamNotFound(stream_name.to_owned()))?;
 
-    let ingestor_stats: Option<Vec<QueriedStats>> = None;
+    let ingestor_stats = if PARSEABLE
+        .get_stream(stream_name)
+        .is_ok_and(|stream| stream.get_stream_type() == StreamType::UserDefined)
+    {
+        Some(fetch_stats_from_ingestors(stream_name).await?)
+    } else {
+        None
+    };
 
     let time = Utc::now();
 
     let stats = {
         let ingestion_stats = IngestionStats::new(
             stats.current_stats.events,
-            format!("{} {}", stats.current_stats.ingestion, "Bytes"),
+            stats.current_stats.ingestion,
             stats.lifetime_stats.events,
-            format!("{} {}", stats.lifetime_stats.ingestion, "Bytes"),
+            stats.lifetime_stats.ingestion,
             stats.deleted_stats.events,
-            format!("{} {}", stats.deleted_stats.ingestion, "Bytes"),
+            stats.deleted_stats.ingestion,
             "json",
         );
         let storage_stats = StorageStats::new(
-            format!("{} {}", stats.current_stats.storage, "Bytes"),
-            format!("{} {}", stats.lifetime_stats.storage, "Bytes"),
-            format!("{} {}", stats.deleted_stats.storage, "Bytes"),
+            stats.current_stats.storage,
+            stats.lifetime_stats.storage,
+            stats.deleted_stats.storage,
             "parquet",
         );
 
diff --git a/src/users/dashboards.rs b/src/users/dashboards.rs
index 39dd785d0..15f5979ef 100644
--- a/src/users/dashboards.rs
+++ b/src/users/dashboards.rs
@@ -16,15 +16,14 @@
  *
  */
 
-use std::sync::RwLock;
-
 use once_cell::sync::Lazy;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
+use tokio::sync::RwLock;
 
 use crate::{
-    migration::to_bytes, parseable::PARSEABLE, storage::object_storage::dashboard_path,
-    utils::get_hash, LOCK_EXPECT,
+    alerts::alerts_utils::user_auth_for_query, migration::to_bytes, parseable::PARSEABLE,
+    rbac::map::SessionKey, storage::object_storage::dashboard_path, utils::get_hash,
 };
 
 use super::TimeFilter;
@@ -172,27 +171,27 @@ impl Dashboards {
             }
         }
 
-        let mut s = self.0.write().expect(LOCK_EXPECT);
+        let mut s = self.0.write().await;
         s.append(&mut this);
 
         Ok(())
     }
 
-    pub fn update(&self, dashboard: &Dashboard) {
-        let mut s = self.0.write().expect(LOCK_EXPECT);
+    pub async fn update(&self, dashboard: &Dashboard) {
+        let mut s = self.0.write().await;
         s.retain(|d| d.dashboard_id != dashboard.dashboard_id);
         s.push(dashboard.clone());
     }
 
-    pub fn delete_dashboard(&self, dashboard_id: &str) {
-        let mut s = self.0.write().expect(LOCK_EXPECT);
+    pub async fn delete_dashboard(&self, dashboard_id: &str) {
+        let mut s = self.0.write().await;
         s.retain(|d| d.dashboard_id != Some(dashboard_id.to_string()));
     }
 
-    pub fn get_dashboard(&self, dashboard_id: &str, user_id: &str) -> Option<Dashboard> {
+    pub async fn get_dashboard(&self, dashboard_id: &str, user_id: &str) -> Option<Dashboard> {
         self.0
             .read()
-            .expect(LOCK_EXPECT)
+            .await
             .iter()
             .find(|d| {
                 d.dashboard_id == Some(dashboard_id.to_string())
@@ -201,14 +200,28 @@ impl Dashboards {
             .cloned()
     }
 
-    pub fn list_dashboards_by_user(&self, user_id: &str) -> Vec<Dashboard> {
-        self.0
-            .read()
-            .expect(LOCK_EXPECT)
-            .iter()
-            .filter(|d| d.user_id == Some(user_id.to_string()))
-            .cloned()
-            .collect()
+    pub async fn list_dashboards(&self, key: &SessionKey) -> Vec<Dashboard> {
+        let read = self.0.read().await;
+
+        let mut dashboards = Vec::new();
+
+        for d in read.iter() {
+            let mut skip_dashboard = false;
+            for tile in d.tiles.iter() {
+                let query = &tile.query;
+                match user_auth_for_query(key, query).await {
+                    Ok(_) => {}
+                    Err(_) => {
+                        skip_dashboard = true;
+                        break;
+                    }
+                }
+            }
+            if !skip_dashboard {
+                dashboards.push(d.clone());
+            }
+        }
+        dashboards
     }
 }
 
diff --git a/src/users/filters.rs b/src/users/filters.rs
index 650be18a4..59a06dff0 100644
--- a/src/users/filters.rs
+++ b/src/users/filters.rs
@@ -19,12 +19,12 @@
 use once_cell::sync::Lazy;
 use serde::{Deserialize, Serialize};
 use serde_json::Value;
-use std::sync::RwLock;
+use tokio::sync::RwLock;
 
 use super::TimeFilter;
 use crate::{
-    migration::to_bytes, parseable::PARSEABLE, storage::object_storage::filter_path,
-    utils::get_hash, LOCK_EXPECT,
+    alerts::alerts_utils::user_auth_for_query, migration::to_bytes, parseable::PARSEABLE,
+    rbac::map::SessionKey, storage::object_storage::filter_path, utils::get_hash,
 };
 
 pub static FILTERS: Lazy<Filters> = Lazy::new(Filters::default);
@@ -124,27 +124,27 @@ impl Filters {
             }
         }
 
-        let mut s = self.0.write().expect(LOCK_EXPECT);
+        let mut s = self.0.write().await;
         s.append(&mut this);
 
         Ok(())
     }
 
-    pub fn update(&self, filter: &Filter) {
-        let mut s = self.0.write().expect(LOCK_EXPECT);
+    pub async fn update(&self, filter: &Filter) {
+        let mut s = self.0.write().await;
         s.retain(|f| f.filter_id != filter.filter_id);
         s.push(filter.clone());
     }
 
-    pub fn delete_filter(&self, filter_id: &str) {
-        let mut s = self.0.write().expect(LOCK_EXPECT);
+    pub async fn delete_filter(&self, filter_id: &str) {
+        let mut s = self.0.write().await;
         s.retain(|f| f.filter_id != Some(filter_id.to_string()));
     }
 
-    pub fn get_filter(&self, filter_id: &str, user_id: &str) -> Option<Filter> {
+    pub async fn get_filter(&self, filter_id: &str, user_id: &str) -> Option<Filter> {
         self.0
             .read()
-            .expect(LOCK_EXPECT)
+            .await
             .iter()
             .find(|f| {
                 f.filter_id == Some(filter_id.to_string()) && f.user_id == Some(user_id.to_string())
@@ -152,14 +152,23 @@ impl Filters {
             .cloned()
     }
 
-    pub fn list_filters_by_user(&self, user_id: &str) -> Vec<Filter> {
-        self.0
-            .read()
-            .expect(LOCK_EXPECT)
-            .iter()
-            .filter(|f| f.user_id == Some(user_id.to_string()))
-            .cloned()
-            .collect()
+    pub async fn list_filters(&self, key: &SessionKey) -> Vec<Filter> {
+        let read = self.0.read().await;
+
+        let mut filters = Vec::new();
+
+        for f in read.iter() {
+            let query = if let Some(q) = &f.query.filter_query {
+                q
+            } else {
+                continue;
+            };
+
+            if (user_auth_for_query(key, query).await).is_ok() {
+                filters.push(f.clone())
+            }
+        }
+        filters
     }
 }
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (9)</summary><blockquote>

<details>
<summary>src/prism/logstream/mod.rs (1)</summary><blockquote>

`99-106`: **Add logging or comments for non-user-defined streams.**

If the stream type is not `UserDefined`, the code quietly returns `None` for `ingestor_stats`. Consider logging a debugging message or adding a comment to clarify that ingestor retrieval is skipped for these streams.

</blockquote></details>
<details>
<summary>src/handlers/http/cluster/mod.rs (1)</summary><blockquote>

`370-394`: **Potential performance concerns with large datasets.**

The function iterates over all manifests to accumulate daily stats. For large `stream_meta_list`, this could become slow. Consider indexing or short-circuiting if you have performance constraints.

</blockquote></details>
<details>
<summary>src/prism/home/mod.rs (1)</summary><blockquote>

`189-195`: **Possible performance issue when cloning large HashMap**  
You are cloning `stream_wise_ingestor_stream_json` for each date iteration, which can be expensive if there are many streams or large metadata. Consider refactoring to pass references or to compute stats without cloning.

</blockquote></details>
<details>
<summary>src/users/filters.rs (2)</summary><blockquote>

`127-128`: **Concurrent updates/deletes via async RwLock**  
Using `async fn update` and `async fn delete_filter` with `await` on a write-locked vector is straightforward. Note that all reads and writes are serialized on the same lock, which might limit concurrency if the number of filters grows large.  

Consider storing filters in a map keyed by filter_id or user_id to reduce lock contention during frequent updates.


Also applies to: 133-137, 139-142

---

`155-172`: **`list_filters` with async authorization**  
The approach of skipping filters lacking a `filter_query` and then auth-checking is sensible. If further concurrency is desired, consider parallelizing the user_auth_for_query checks, but ensure that it doesn’t introduce undue complexity.

</blockquote></details>
<details>
<summary>src/handlers/http/users/filters.rs (1)</summary><blockquote>

`146-147`: **`Custom` error variant for session key extraction**  
Introducing a custom error variant lets you surface more specific problems (e.g., session key parse failures). The chosen status code of 500 might be reconsidered if user input errors are to be treated differently.  



Also applies to: 157-157

</blockquote></details>
<details>
<summary>src/handlers/http/cluster/utils.rs (1)</summary><blockquote>

`132-132`: **Nitpick: Consider renaming the function to fix the spelling mistake.**

The function name `merge_quried_stats` contains a minor typographical error. Updating it to `merge_queried_stats` would improve clarity.

```diff
-pub fn merge_quried_stats(stats: Vec<QueriedStats>) -> QueriedStats {
+pub fn merge_queried_stats(stats: Vec<QueriedStats>) -> QueriedStats {
```

</blockquote></details>
<details>
<summary>src/users/dashboards.rs (1)</summary><blockquote>

`203-225`: **Performance note: Potential optimization for multi-tile dashboards.**

The loop checks user authorization (`user_auth_for_query`) against every tile in a dashboard to decide if the entire dashboard should be discarded. If there are many dashboards with many tiles, this can be expensive. Consider caching or pre-checking if the final user’s session scope covers the queries, or short-circuit once a tile fails.

</blockquote></details>
<details>
<summary>src/handlers/http/users/dashboards.rs (1)</summary><blockquote>

`168-169`: **Consider returning more precise HTTP codes for custom errors.**

All custom errors currently map to `INTERNAL_SERVER_ERROR`, which might conflate unrelated conditions. Returning more specific status codes such as `403 Forbidden` or `400 Bad Request` can provide better feedback.



Also applies to: 178-178

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8381e72a2e6ee132d7503f2d0a6fba44c1aa2c92 and 5e83d9e7061438e19f22f44a09d76020345e2357.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `src/handlers/http/cluster/mod.rs` (2 hunks)
* `src/handlers/http/cluster/utils.rs` (3 hunks)
* `src/handlers/http/logstream.rs` (1 hunks)
* `src/handlers/http/modal/query/querier_logstream.rs` (4 hunks)
* `src/handlers/http/users/dashboards.rs` (9 hunks)
* `src/handlers/http/users/filters.rs` (9 hunks)
* `src/prism/home/mod.rs` (7 hunks)
* `src/prism/logstream/mod.rs` (2 hunks)
* `src/users/dashboards.rs` (3 hunks)
* `src/users/filters.rs` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (10)</summary>

* GitHub Check: coverage
* GitHub Check: Quest Smoke and Load Tests for Distributed deployments
* GitHub Check: Build Default aarch64-unknown-linux-gnu
* GitHub Check: Build Default x86_64-unknown-linux-gnu
* GitHub Check: Quest Smoke and Load Tests for Standalone deployments
* GitHub Check: Build Default x86_64-pc-windows-msvc
* GitHub Check: Build Default x86_64-apple-darwin
* GitHub Check: Build Kafka x86_64-unknown-linux-gnu
* GitHub Check: Build Default aarch64-apple-darwin
* GitHub Check: Build Kafka aarch64-apple-darwin

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>src/handlers/http/logstream.rs (1)</summary>

`265-275`: **Consider verifying potential integer overflow.**

Now that ingestion & storage stats use direct `u64` numeric fields, extremely large ingestion or storage volumes might cause overflow. If there's a scenario with extremely large data, it might be prudent to incorporate overflow checks or saturating additions.

</details>
<details>
<summary>src/prism/logstream/mod.rs (4)</summary>

`29-32`: **New imports appear consistent.**

These imports are relevant for fetching stats from ingestors. No immediate concerns.

---

`38-38`: **Import usage is aligned with new references.**

`StreamType` is used for determining if a stream is user-defined or not. The usage looks correct.

---

`113-117`: **Direct numeric ingestion fields.**

Switching to raw `u64` ingestion fields is consistent with the rest of the codebase. No issues found.

---

`121-123`: **Direct numeric storage fields.**

Switching to raw `u64` storage fields is consistent as well. No issues found.

</details>
<details>
<summary>src/handlers/http/modal/query/querier_logstream.rs (3)</summary>

`29-29`: **RelativePathBuf import is valid.**

No immediate concerns. This import is necessary for constructing paths to object store resources.

---

`48-48`: **Additional import for `StreamType` and `STREAM_ROOT_DIRECTORY`.**

This import aligns with new usage in the code. No issues found.

---

`207-217`: **Check for potential overflow in large-scale scenarios.**

Since these fields directly add to `u64` counters, ensure there's no risk of overflow under high ingestion volumes. Adding a saturating or overflow check might be prudent if extremely large data is expected.

</details>
<details>
<summary>src/prism/home/mod.rs (4)</summary>

`19-25`: **Imports updated to accommodate fetch and stats operations**  
These additional imports (e.g., `HashMap`, `RelativePathBuf`, `fetch_daily_stats_from_ingestors`, and `get_stats_date`) seem necessary for the new functionality. No concerns here.  



Also applies to: 31-34, 38-38

---

`201-201`: **Including `stream_titles.clone()` in HomeResponse**  
No issues in reusing the same vector for response. This supports consistency in your UI.

---

`210-230`: **`stats_for_date` function logic**  
The async function succinctly aggregates querier and ingestor stats. The approach is clear, but be mindful of error handling if partial data is acceptable or not. Currently, a single stream's error aborts the whole process.  

Would you like to allow partial success if one of the streams fails?

---

`242-243`: **New error variant introduced**  
Adding `ObjectStorageError` to the local error type integrates well with existing `ResponseError` handling. This ensures relevant HTTP 500 responses when storage errors occur.  



Also applies to: 254-254

</details>
<details>
<summary>src/users/filters.rs (2)</summary>

`22-22`: **Switched to `tokio::sync::RwLock` and introduced additional utility functions**  
Shifting from a synchronous RwLock to an async one is suitable for asynchronous contexts. Likewise, referencing new modules (e.g., `user_auth_for_query`) aligns with the new flow.  



Also applies to: 26-28

---

`144-144`: **Asynchronous `get_filter`**  
Reading from the RwLock is now asynchronous. This is consistent with the rest of the codebase and is properly awaited.  



Also applies to: 147-148

</details>
<details>
<summary>src/handlers/http/users/filters.rs (2)</summary>

`36-39`: **Extraction of session key for listing filters**  
Replacing manual user ID handling with `extract_session_key_from_req` is consistent with the new session-based approach. Converting the extraction error to `FiltersError::Custom` also simplifies error handling.

---

`50-50`: **Async usage of `FILTERS`**  
These updates ensure that `get_filter`, `update`, and `delete_filter` are performed asynchronously. This is correct for an async environment and helps align filter operations with other asynchronous flows.  



Also applies to: 67-67, 90-90, 96-96, 120-121, 131-131

</details>
<details>
<summary>src/handlers/http/cluster/utils.rs (3)</summary>

`83-88`: **Enhancement: Switching size fields to unsigned 64-bit integers is a good move.**

All changes from `String` to `u64` for ingestion stats (size, lifetime_size, deleted_size) strengthen type safety and reduce overhead by avoiding string parsing. Good job on this update!



Also applies to: 94-98

---

`115-118`: **Consistent numeric usage in storage stats.**

Changing the storage fields (`size`, `lifetime_size`, `deleted_size`) to `u64` ensures consistency with the ingestion stats and avoids runtime conversions. This simplifies the codebase and improves performance.



Also applies to: 122-122, 158-162

---

`134-134`: <details>
<summary>❓ Verification inconclusive</summary>

**Potential out-of-bounds panic.**

The code uses `stats[1]` to retrieve the stream name. If the `stats` vector is empty or has only one element, this will panic. Consider using safe indexing or a check on the length of `stats`.


```diff
-let stream_name = stats[1].stream.clone();
+let stream_name = if stats.len() > 1 {
+    stats[1].stream.clone()
+} else if !stats.is_empty() {
+    stats[0].stream.clone()
+} else {
+    "unknown_stream".to_string()
+};
```

---



</details>

**Action Required: Validate Safe-Indexing Logic for `stats`**

The current fix prevents a potential out-of-bounds panic by checking the length of `stats`. The diff snippet ensures that if `stats` contains more than one element, the second element is used; if only one element exists, it falls back to using that single element; otherwise, it defaults to `"unknown_stream"`. Please confirm that:

- Falling back to `stats[0].stream` when there’s exactly one element aligns with the intended logic.
- The default value `"unknown_stream"` is appropriate in cases where `stats` is empty.

```diff
-let stream_name = stats[1].stream.clone();
+let stream_name = if stats.len() > 1 {
+    stats[1].stream.clone()
+} else if !stats.is_empty() {
+    stats[0].stream.clone()
+} else {
+    "unknown_stream".to_string()
+};
```

</details>
<details>
<summary>src/users/dashboards.rs (1)</summary>

`22-22`: **Good choice using asynchronous RwLock.**

Replacing `std::sync::RwLock` with `tokio::sync::RwLock` aligns with async operations. This helps avoid blocking the entire thread during reads and writes.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:21:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/handlers/http/modal/query/querier_logstream.rs (1)</summary><blockquote>

`185-188`: **Consider adding more context to error log.**

When logging a parsing error, consider including the stream name or file path to make troubleshooting easier.

```diff
-error!("Failed to parse stream metadata: {:?}", e);
+error!("Failed to parse stream metadata for stream {}: {:?}", stream_name, e);
```

</blockquote></details>
<details>
<summary>src/prism/home/mod.rs (1)</summary><blockquote>

`196-196`: **Avoid unnecessary cloning of HashMap.**

The `stream_wise_ingestor_stream_json.clone()` creates a full copy of the HashMap on each iteration, which may impact performance, especially with many streams.


```diff
-let dated_stats = stats_for_date(date, stream_wise_ingestor_stream_json.clone()).await?;
+let dated_stats = stats_for_date(date, &stream_wise_ingestor_stream_json).await?;
```

And update the function signature:

```diff
-async fn stats_for_date(
-    date: String,
-    stream_wise_meta: HashMap<String, Vec<ObjectStoreFormat>>,
-) -> Result<DatedStats, PrismHomeError> {
+async fn stats_for_date(
+    date: String,
+    stream_wise_meta: &HashMap<String, Vec<ObjectStoreFormat>>,
+) -> Result<DatedStats, PrismHomeError> {
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5e83d9e7061438e19f22f44a09d76020345e2357 and 233f8ba4bf831b977f46e4ad2fc25987cc3c421d.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `src/handlers/http/cluster/mod.rs` (2 hunks)
* `src/handlers/http/cluster/utils.rs` (3 hunks)
* `src/handlers/http/logstream.rs` (1 hunks)
* `src/handlers/http/modal/query/querier_logstream.rs` (4 hunks)
* `src/handlers/http/users/dashboards.rs` (9 hunks)
* `src/handlers/http/users/filters.rs` (9 hunks)
* `src/prism/home/mod.rs` (7 hunks)
* `src/prism/logstream/mod.rs` (2 hunks)
* `src/users/dashboards.rs` (3 hunks)
* `src/users/filters.rs` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (7)</summary>

* src/prism/logstream/mod.rs
* src/handlers/http/logstream.rs
* src/users/filters.rs
* src/handlers/http/cluster/utils.rs
* src/handlers/http/users/filters.rs
* src/handlers/http/users/dashboards.rs
* src/users/dashboards.rs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (10)</summary>

* GitHub Check: Build Default x86_64-unknown-linux-gnu
* GitHub Check: Build Default aarch64-unknown-linux-gnu
* GitHub Check: Build Default x86_64-apple-darwin
* GitHub Check: Build Kafka x86_64-unknown-linux-gnu
* GitHub Check: Build Default x86_64-pc-windows-msvc
* GitHub Check: Build Kafka aarch64-apple-darwin
* GitHub Check: Build Default aarch64-apple-darwin
* GitHub Check: Quest Smoke and Load Tests for Standalone deployments
* GitHub Check: Quest Smoke and Load Tests for Distributed deployments
* GitHub Check: coverage

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>src/handlers/http/modal/query/querier_logstream.rs (3)</summary>

`29-29`: **Good addition of RelativePathBuf for path handling.**

The addition of `relative_path::RelativePathBuf` is a good choice for handling paths in a cross-platform manner and working with object storage paths.

---

`156-182`: **Improved error handling for stream metadata parsing.**

The implementation now properly handles JSON parsing errors instead of using `.expect()`, which prevents runtime panics when encountering malformed JSON files. This is a significant improvement for application stability.


The change from HTTP requests to direct object store access should also improve performance by reducing network overhead.

---

`209-223`: **Improved type safety by using raw numeric values.**

The code now passes raw numeric values to the `IngestionStats` and `StorageStats` constructors instead of formatted strings. This is more type-safe and avoids unnecessary string conversions.

</details>
<details>
<summary>src/handlers/http/cluster/mod.rs (3)</summary>

`370-395`: **Good refactoring of fetch_daily_stats_from_ingestors to be synchronous.**

Changing this function from async to synchronous and modifying its parameters to accept pre-fetched metadata is a good design improvement. This makes the code:

1. More efficient by eliminating unnecessary HTTP requests
2. More flexible by allowing callers to control how metadata is retrieved
3. Easier to test by reducing external dependencies

The implementation effectively iterates through the metadata to accumulate statistics for the specified date.

---

`423-425`: _⚠️ Potential issue_

**Add error handling to prevent panic on invalid JSON.**

Using `.expect()` here can cause runtime panics if the JSON is malformed. This is inconsistent with the error handling patterns used elsewhere in the codebase.



```diff
-let stream_metadata: ObjectStoreFormat =
-    serde_json::from_slice(&ob).expect("stream.json is valid json");
+let stream_metadata: ObjectStoreFormat = match serde_json::from_slice(&ob) {
+    Ok(metadata) => metadata,
+    Err(e) => {
+        error!("Failed to parse stream metadata: {:?}", e);
+        continue;
+    }
+};
```

---

`437-454`: **Improved type safety in statistic constructors.**

The code now uses raw numeric values in the `IngestionStats` and `StorageStats` constructors, which is a better design than using formatted strings. This provides:

1. Better type safety
2. Reduced conversion overhead
3. More flexibility for formatting at the display layer

</details>
<details>
<summary>src/prism/home/mod.rs (6)</summary>

`19-40`: **Good module organization with proper imports.**

The addition of `HashMap` and other necessary imports helps structure the code for better data management.

---

`118-120`: **Simplified dashboard and filter retrieval using session key.**

Using session keys directly for dashboard and filter retrieval improves the code by:
1. Removing unnecessary user ID extraction
2. Reducing potential error paths
3. Making authorization more consistent across the codebase



Also applies to: 134-136

---

`167-193`: **Well-structured metadata collection with proper error handling.**

The implementation now:
1. Effectively uses a HashMap to organize stream metadata
2. Properly handles JSON parsing errors without panicking
3. Improves organization and readability

This is a significant improvement over nested loops for better maintainability.

---

`195-202`: **Efficient date-wise statistics processing.**

Using the new helper function `stats_for_date` creates a cleaner, more modular design. The stream summary accumulation is straightforward and easy to understand.

---

`216-236`: **Good separation of concerns with stats_for_date helper function.**

Creating a dedicated function for date-specific statistics aggregation improves code organization and readability. This function effectively:

1. Collects stats for all streams for a given date
2. Aggregates querier and ingestor statistics
3. Follows the single-responsibility principle

The code is well-structured and properly handles errors.

---

`248-250`: **Properly extended error handling for ObjectStorageError.**

Adding the new variant to `PrismHomeError` improves error handling and propagation throughout the application. This ensures that object storage errors are properly reported and handled.



Also applies to: 259-259

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:36:37Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T11:36:42Z
user: login: nikhilsinhaparseable
body: looks good
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T03:30:05Z

Review comments:
diff_hunk: @@ -151,7 +152,30 @@ pub async fn get_stats(
 
         if !date_value.is_empty() {
             let querier_stats = get_stats_date(&stream_name, date_value).await?;
-            let ingestor_stats = fetch_daily_stats_from_ingestors(&stream_name, date_value).await?;
+
+            // this function requires all the ingestor stream jsons
+            let path = RelativePathBuf::from_iter([&stream_name, STREAM_ROOT_DIRECTORY]);
+            let obs = PARSEABLE
+                .storage
+                .get_object_store()
+                .get_objects(
+                    Some(&path),
+                    Box::new(|file_name| {
+                        file_name.starts_with(".ingestor") && file_name.ends_with("stream.json")
+                    }),
+                )
+                .await?;
+
+            let mut ingestor_stream_jsons = Vec::new();
+            for ob in obs {
+                let stream_metadata: ObjectStoreFormat =
+                    serde_json::from_slice(&ob).expect("stream.json is valid json");
+                ingestor_stream_jsons.push(stream_metadata);
+            }
+
+            let ingestor_stats =
+                fetch_daily_stats_from_ingestors(date_value, &ingestor_stream_jsons)?;
+
path: src/handlers/http/modal/query/querier_logstream.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Avoid using `.expect(...)` in production code.**

Using `.expect("stream.json is valid json")` can cause runtime panics if the file is malformed or corrupted. Instead, consider handling or logging deserialization errors to avoid crashing the server.

Here's a possible fix to avoid panics:

```diff
- let stream_metadata: ObjectStoreFormat =
-     serde_json::from_slice(&ob).expect("stream.json is valid json");
+ let stream_metadata: ObjectStoreFormat = match serde_json::from_slice(&ob) {
+     Ok(meta) => meta,
+     Err(err) => {
+         warn!("Invalid stream.json encountered: {}", err);
+         continue;
+     }
+ };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            // this function requires all the ingestor stream jsons
            let path = RelativePathBuf::from_iter([&stream_name, STREAM_ROOT_DIRECTORY]);
            let obs = PARSEABLE
                .storage
                .get_object_store()
                .get_objects(
                    Some(&path),
                    Box::new(|file_name| {
                        file_name.starts_with(".ingestor") && file_name.ends_with("stream.json")
                    }),
                )
                .await?;

            let mut ingestor_stream_jsons = Vec::new();
            for ob in obs {
-                let stream_metadata: ObjectStoreFormat =
-                    serde_json::from_slice(&ob).expect("stream.json is valid json");
+                let stream_metadata: ObjectStoreFormat = match serde_json::from_slice(&ob) {
+                    Ok(meta) => meta,
+                    Err(err) => {
+                        warn!("Invalid stream.json encountered: {}", err);
+                        continue;
+                    }
+                };
                 ingestor_stream_jsons.push(stream_metadata);
             }

            let ingestor_stats =
                fetch_daily_stats_from_ingestors(date_value, &ingestor_stream_jsons)?;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-05T11:21:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -159,35 +160,45 @@ pub async fn generate_home_response(key: &SessionKey) -> Result<HomeResponse, Pr
 
     let mut stream_details = Vec::new();
 
+    // this will hold the summary of all streams for the last 7 days
     let mut summary = StreamInfo::default();
path: src/prism/home/mod.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Panic risk on JSON parsing**  
Calling `.expect("stream.json is valid json")` can lead to a panic if the data isn't valid JSON. Consider handling this gracefully (e.g., logging an error and skipping the file) instead of panicking.  

```diff
-for ob in obs {
-    let stream_metadata: ObjectStoreFormat =
-        serde_json::from_slice(&ob).expect("stream.json is valid json");
-    ingestor_stream_jsons.push(stream_metadata);
-}
+for ob in obs {
+    let stream_metadata: ObjectStoreFormat = match serde_json::from_slice(&ob) {
+        Ok(parsed) => parsed,
+        Err(e) => {
+            log::error!("Invalid stream.json format: {}", e);
+            continue; // Skip this file to prevent a panic
+        }
+    };
+    ingestor_stream_jsons.push(stream_metadata);
+}
```


Also applies to: 166-187

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-05T11:21:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,7 +21,7 @@ use crate::{
     parseable::PARSEABLE,
     storage::{object_storage::dashboard_path, ObjectStorageError},
     users::dashboards::{Dashboard, CURRENT_DASHBOARD_VERSION, DASHBOARDS},
-    utils::{get_hash, get_user_from_request},
+    utils::{actix::extract_session_key_from_req, get_hash, get_user_from_request},
 };
path: src/handlers/http/users/dashboards.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inconsistent usage of user retrieval vs. session key.**

You are importing both `extract_session_key_from_req` and `get_user_from_request`. The `list` method uses the session key for authorization checks, but the `get` method still leverages the user ID. If feasible, unify these approaches to keep dashboard access consistent.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-05T11:21:01Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-05T11:16:41Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request makes significant modifications across multiple modules to revise statistical aggregation, asynchronous operations, and type safety. It reworks the functions for fetching and computing ingestion metrics—shifting from asynchronous HTTP calls to synchronous metadata accumulation and converting size fields from strings to numeric types. Additionally, the PR updates dashboard and filter handling to use session key extraction with asynchronous operations and improved error variants, while also introducing a helper function for aggregating stream statistics in the Prism home module.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `src/handlers/http/cluster/mod.rs`, `src/handlers/http/cluster/utils.rs`, `src/handlers/http/logstream.rs`, `src/handlers/http/modal/query/querier_logstream.rs`, `src/prism/logstream/mod.rs` | Refactored statistics computation: changed functions from async to sync; updated parameters from stream name to metadata list; converted size fields from `String` to `u64`; removed string formatting for sizes; and added conditional ingestor stats fetching based on stream type. |
| `src/handlers/http/users/dashboards.rs`, `src/handlers/http/users/filters.rs`, `src/users/dashboards.rs`, `src/users/filters.rs` | Updated session extraction and error handling: replaced user ID retrieval with session key extraction; transitioned dashboard and filter operations to asynchronous flows using `tokio::sync::RwLock`; and introduced new `Custom` variants in error enums. |
| `src/prism/home/mod.rs` | Simplified stream statistics aggregation: removed user ID checks, updated dashboard/filter retrieval to use session keys, restructured statistics computation using a new helper function `stats_for_date`, and enhanced error handling with an `ObjectStorageError` variant. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant App
    participant FetchDailyStats
    App->>FetchDailyStats: call fetch_daily_stats_from_ingestors(date, stream_meta_list)
    loop For each manifest in stream_meta_list
        FetchDailyStats->>FetchDailyStats: Check manifest date & accumulate events, ingestion, storage sizes
    end
    FetchDailyStats->>App: Return computed Stats
```

```mermaid
sequenceDiagram
    participant Client
    participant SessionExtractor
    participant DashboardHandler
    participant FilterHandler

    Client->>SessionExtractor: extract_session_key_from_req(request)
    SessionExtractor-->>DashboardHandler: Process session key for dashboards
    DashboardHandler->>DashboardHandler: Async operations (list, get, update, delete)
    DashboardHandler-->>Client: Return dashboard response

    Client->>SessionExtractor: extract_session_key_from_req(request)
    SessionExtractor-->>FilterHandler: Process session key for filters
    FilterHandler->>FilterHandler: Async operations (get, update, delete, list)
    FilterHandler-->>Client: Return filter response
```

## Possibly related PRs
- **parseablehq/parseable#1147**: The changes in the main PR, specifically the restructuring of the `fetch_daily_stats_from_ingestors` function and the simplification of statistics handling, are related to the modifications in the retrieved PR, which also involves changes to how statistics are processed and represented, particularly in the context of object storage.
- **parseablehq/parseable#1188**: The changes in the main PR, specifically the modifications to the `fetch_daily_stats_from_ingestors` function and the handling of statistics, are related to the changes in the retrieved PR, which also involve the handling of statistics in the `get_stats` function, particularly in how statistics are constructed and formatted.
- **parseablehq/parseable#1209**: The changes in the main PR, specifically the restructuring of the `fetch_daily_stats_from_ingestors` function and the simplification of statistics handling, are related to the modifications in the retrieved PR, which also involve streamlining how statistics are constructed and passed in the context of event handling.

## Suggested labels
`for next release`

## Suggested reviewers
- nikhilsinhaparseable

## Poem
> I'm a rabbit in the code burrow,  
> Hopping through async streams today,  
> Stats and dashboards all in tow,  
> Numeric types lead the way, 🐇  
> Celebrating changes in a joyful display!  
> Coding carrots for a brighter day!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=parseablehq/parseable&utm_content=1224):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: de-sh
created_at: 2025-03-06T06:44:20Z
body: This PR also solves #1187 
author_association: CONTRIBUTOR