From 24d154248c8814e729206208f096aba68dcff8c0 Mon Sep 17 00:00:00 2001 From: Vrishab Srivatsa <136090360+vsrivatsa-juspay@users.noreply.github.com> Date: Fri, 10 May 2024 15:38:26 +0530 Subject: [PATCH] feat(analytics): authentication analytics (#4429) Co-authored-by: Sampras Lopes Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/analytics/src/auth_events.rs | 6 + .../analytics/src/auth_events/accumulator.rs | 58 +++++++++ crates/analytics/src/auth_events/core.rs | 108 +++++++++++++++++ crates/analytics/src/auth_events/metrics.rs | 107 +++++++++++++++++ .../metrics/authentication_attempt_count.rs} | 57 +++------ .../metrics/authentication_success_count.rs} | 55 +++------ .../metrics/challenge_attempt_count.rs | 90 ++++++++++++++ .../metrics/challenge_flow_count.rs} | 55 +++------ .../metrics/challenge_success_count.rs} | 60 +++------- .../metrics/frictionless_flow_count.rs} | 59 ++++------ .../metrics/three_ds_sdk_count.rs} | 55 +++------ crates/analytics/src/clickhouse.rs | 36 +++++- crates/analytics/src/core.rs | 5 + crates/analytics/src/lib.rs | 34 ++++++ crates/analytics/src/query.rs | 30 ++++- .../analytics/src/sdk_events/accumulator.rs | 18 +-- crates/analytics/src/sdk_events/core.rs | 24 +--- crates/analytics/src/sdk_events/metrics.rs | 92 +-------------- .../metrics/average_payment_time.rs | 12 +- ...rictionless_flow_count.rs => load_time.rs} | 19 ++- crates/analytics/src/sqlx.rs | 20 +++- crates/analytics/src/types.rs | 2 + crates/analytics/src/utils.rs | 5 + crates/api_models/src/analytics.rs | 13 ++ .../api_models/src/analytics/auth_events.rs | 111 ++++++++++++++++++ crates/api_models/src/analytics/sdk_events.rs | 27 +---- crates/api_models/src/events.rs | 3 +- crates/router/src/analytics.rs | 47 +++++++- 28 files changed, 790 insertions(+), 418 deletions(-) create mode 100644 crates/analytics/src/auth_events.rs create mode 100644 crates/analytics/src/auth_events/accumulator.rs create mode 100644 crates/analytics/src/auth_events/core.rs create mode 100644 crates/analytics/src/auth_events/metrics.rs rename crates/analytics/src/{sdk_events/metrics/three_ds_method_invoked_count.rs => auth_events/metrics/authentication_attempt_count.rs} (58%) rename crates/analytics/src/{sdk_events/metrics/authentication_unsuccessful_count.rs => auth_events/metrics/authentication_success_count.rs} (59%) create mode 100644 crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs rename crates/analytics/src/{sdk_events/metrics/three_ds_challenge_flow_count.rs => auth_events/metrics/challenge_flow_count.rs} (63%) rename crates/analytics/src/{sdk_events/metrics/three_ds_method_unsuccessful_count.rs => auth_events/metrics/challenge_success_count.rs} (52%) rename crates/analytics/src/{sdk_events/metrics/three_ds_method_skipped_count.rs => auth_events/metrics/frictionless_flow_count.rs} (59%) rename crates/analytics/src/{sdk_events/metrics/three_ds_method_successful_count.rs => auth_events/metrics/three_ds_sdk_count.rs} (62%) rename crates/analytics/src/sdk_events/metrics/{three_ds_frictionless_flow_count.rs => load_time.rs} (87%) create mode 100644 crates/api_models/src/analytics/auth_events.rs diff --git a/crates/analytics/src/auth_events.rs b/crates/analytics/src/auth_events.rs new file mode 100644 index 0000000000..e708c3c883 --- /dev/null +++ b/crates/analytics/src/auth_events.rs @@ -0,0 +1,6 @@ +pub mod accumulator; +mod core; +pub mod metrics; +pub use accumulator::{AuthEventMetricAccumulator, AuthEventMetricsAccumulator}; + +pub use self::core::get_metrics; diff --git a/crates/analytics/src/auth_events/accumulator.rs b/crates/analytics/src/auth_events/accumulator.rs new file mode 100644 index 0000000000..409e805af8 --- /dev/null +++ b/crates/analytics/src/auth_events/accumulator.rs @@ -0,0 +1,58 @@ +use api_models::analytics::auth_events::AuthEventMetricsBucketValue; + +use super::metrics::AuthEventMetricRow; + +#[derive(Debug, Default)] +pub struct AuthEventMetricsAccumulator { + pub three_ds_sdk_count: CountAccumulator, + pub authentication_attempt_count: CountAccumulator, + pub authentication_success_count: CountAccumulator, + pub challenge_flow_count: CountAccumulator, + pub challenge_attempt_count: CountAccumulator, + pub challenge_success_count: CountAccumulator, + pub frictionless_flow_count: CountAccumulator, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +pub trait AuthEventMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl AuthEventMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl AuthEventMetricsAccumulator { + pub fn collect(self) -> AuthEventMetricsBucketValue { + AuthEventMetricsBucketValue { + three_ds_sdk_count: self.three_ds_sdk_count.collect(), + authentication_attempt_count: self.authentication_attempt_count.collect(), + authentication_success_count: self.authentication_success_count.collect(), + challenge_flow_count: self.challenge_flow_count.collect(), + challenge_attempt_count: self.challenge_attempt_count.collect(), + challenge_success_count: self.challenge_success_count.collect(), + frictionless_flow_count: self.frictionless_flow_count.collect(), + } + } +} diff --git a/crates/analytics/src/auth_events/core.rs b/crates/analytics/src/auth_events/core.rs new file mode 100644 index 0000000000..761a95bb9b --- /dev/null +++ b/crates/analytics/src/auth_events/core.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier, MetricsBucketResponse}, + AnalyticsMetadata, GetAuthEventMetricRequest, MetricsResponse, +}; +use error_stack::ResultExt; +use router_env::{instrument, logger, tracing}; + +use super::AuthEventMetricsAccumulator; +use crate::{ + auth_events::AuthEventMetricAccumulator, + errors::{AnalyticsError, AnalyticsResult}, + AnalyticsProvider, +}; + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &String, + publishable_key: Option<&String>, + req: GetAuthEventMetricRequest, +) -> AnalyticsResult> { + let mut metrics_accumulator: HashMap< + AuthEventMetricsBucketIdentifier, + AuthEventMetricsAccumulator, + > = HashMap::new(); + + if let Some(publishable_key) = publishable_key { + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let merchant_id_scoped = merchant_id.to_owned(); + let publishable_key_scoped = publishable_key.to_owned(); + let pool = pool.clone(); + set.spawn(async move { + let data = pool + .get_auth_event_metrics( + &metric_type, + &merchant_id_scoped, + &publishable_key_scoped, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + }); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .change_context(AnalyticsError::UnknownError)? + { + for (id, value) in data? { + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + AuthEventMetrics::ThreeDsSdkCount => metrics_builder + .three_ds_sdk_count + .add_metrics_bucket(&value), + AuthEventMetrics::AuthenticationAttemptCount => metrics_builder + .authentication_attempt_count + .add_metrics_bucket(&value), + AuthEventMetrics::AuthenticationSuccessCount => metrics_builder + .authentication_success_count + .add_metrics_bucket(&value), + AuthEventMetrics::ChallengeFlowCount => metrics_builder + .challenge_flow_count + .add_metrics_bucket(&value), + AuthEventMetrics::ChallengeAttemptCount => metrics_builder + .challenge_attempt_count + .add_metrics_bucket(&value), + AuthEventMetrics::ChallengeSuccessCount => metrics_builder + .challenge_success_count + .add_metrics_bucket(&value), + AuthEventMetrics::FrictionlessFlowCount => metrics_builder + .frictionless_flow_count + .add_metrics_bucket(&value), + } + } + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } else { + logger::error!("Publishable key not present for merchant ID"); + Ok(MetricsResponse { + query_data: vec![], + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + }) + } +} diff --git a/crates/analytics/src/auth_events/metrics.rs b/crates/analytics/src/auth_events/metrics.rs new file mode 100644 index 0000000000..6830747492 --- /dev/null +++ b/crates/analytics/src/auth_events/metrics.rs @@ -0,0 +1,107 @@ +use api_models::analytics::{ + auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use time::PrimitiveDateTime; + +use crate::{ + query::{Aggregate, GroupByClause, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, +}; + +mod authentication_attempt_count; +mod authentication_success_count; +mod challenge_attempt_count; +mod challenge_flow_count; +mod challenge_success_count; +mod frictionless_flow_count; +mod three_ds_sdk_count; + +use authentication_attempt_count::AuthenticationAttemptCount; +use authentication_success_count::AuthenticationSuccessCount; +use challenge_attempt_count::ChallengeAttemptCount; +use challenge_flow_count::ChallengeFlowCount; +use challenge_success_count::ChallengeSuccessCount; +use frictionless_flow_count::FrictionlessFlowCount; +use three_ds_sdk_count::ThreeDsSdkCount; + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct AuthEventMetricRow { + pub count: Option, + pub time_bucket: Option, +} + +pub trait AuthEventMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait AuthEventMetric +where + T: AnalyticsDataSource + AuthEventMetricAnalytics, +{ + async fn load_metrics( + &self, + merchant_id: &str, + publishable_key: &str, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl AuthEventMetric for AuthEventMetrics +where + T: AnalyticsDataSource + AuthEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + merchant_id: &str, + publishable_key: &str, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::ThreeDsSdkCount => { + ThreeDsSdkCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::AuthenticationAttemptCount => { + AuthenticationAttemptCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::AuthenticationSuccessCount => { + AuthenticationSuccessCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::ChallengeFlowCount => { + ChallengeFlowCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::ChallengeAttemptCount => { + ChallengeAttemptCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::ChallengeSuccessCount => { + ChallengeSuccessCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::FrictionlessFlowCount => { + FrictionlessFlowCount + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + } + } +} diff --git a/crates/analytics/src/sdk_events/metrics/three_ds_method_invoked_count.rs b/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs similarity index 58% rename from crates/analytics/src/sdk_events/metrics/three_ds_method_invoked_count.rs rename to crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs index 17368d8a74..dd65458b1e 100644 --- a/crates/analytics/src/sdk_events/metrics/three_ds_method_invoked_count.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs @@ -1,26 +1,24 @@ use api_models::analytics::{ - sdk_events::{ - SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, - }, - Granularity, TimeRange, + auth_events::AuthEventMetricsBucketIdentifier, sdk_events::SdkEventNames, Granularity, + TimeRange, }; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; -use super::SdkEventMetricRow; +use super::AuthEventMetricRow; use crate::{ query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] -pub(super) struct ThreeDsMethodInvokedCount; +pub(super) struct AuthenticationAttemptCount; #[async_trait::async_trait] -impl super::SdkEventMetric for ThreeDsMethodInvokedCount +impl super::AuthEventMetric for AuthenticationAttemptCount where - T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, PrimitiveDateTime: ToSql, AnalyticsCollection: ToSql, Granularity: GroupByClause, @@ -29,19 +27,13 @@ where { async fn load_metrics( &self, - dimensions: &[SdkEventDimensions], + _merchant_id: &str, publishable_key: &str, - filters: &SdkEventFilters, granularity: &Option, time_range: &TimeRange, pool: &T, - ) -> MetricsResult> { + ) -> MetricsResult> { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); - let dimensions = dimensions.to_vec(); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } query_builder .add_select_column(Aggregate::Count { @@ -56,14 +48,16 @@ where .switch()?; } - filters.set_filter_clause(&mut query_builder).switch()?; - query_builder .add_filter_clause("merchant_id", publishable_key) .switch()?; query_builder - .add_filter_clause("event_name", SdkEventNames::ThreeDsMethod) + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("event_name", SdkEventNames::AuthenticationCallInit) .switch()?; query_builder @@ -71,23 +65,14 @@ where .switch()?; query_builder - .add_filter_clause("category", "USER_EVENT") + .add_filter_clause("category", "API") .switch()?; - query_builder.add_filter_clause("value", "Y").switch()?; - time_range .set_filter_clause(&mut query_builder) .attach_printable("Error filtering time range") .switch()?; - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - if let Some(_granularity) = granularity.as_ref() { query_builder .add_group_by_clause("time_bucket") @@ -96,27 +81,19 @@ where } query_builder - .execute_query::(pool) + .execute_query::(pool) .await .change_context(MetricsError::QueryBuildingError)? .change_context(MetricsError::QueryExecutionFailure)? .into_iter() .map(|i| { Ok(( - SdkEventMetricsBucketIdentifier::new( - i.payment_method.clone(), - i.platform.clone(), - i.browser_name.clone(), - i.source.clone(), - i.component.clone(), - i.payment_experience.clone(), - i.time_bucket.clone(), - ), + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), i, )) }) .collect::, + Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) diff --git a/crates/analytics/src/sdk_events/metrics/authentication_unsuccessful_count.rs b/crates/analytics/src/auth_events/metrics/authentication_success_count.rs similarity index 59% rename from crates/analytics/src/sdk_events/metrics/authentication_unsuccessful_count.rs rename to crates/analytics/src/auth_events/metrics/authentication_success_count.rs index c2fe777d82..7559abe8e2 100644 --- a/crates/analytics/src/sdk_events/metrics/authentication_unsuccessful_count.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_success_count.rs @@ -1,26 +1,24 @@ use api_models::analytics::{ - sdk_events::{ - SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, - }, - Granularity, TimeRange, + auth_events::AuthEventMetricsBucketIdentifier, sdk_events::SdkEventNames, Granularity, + TimeRange, }; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; -use super::SdkEventMetricRow; +use super::AuthEventMetricRow; use crate::{ query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] -pub(super) struct AuthenticationUnsuccessfulCount; +pub(super) struct AuthenticationSuccessCount; #[async_trait::async_trait] -impl super::SdkEventMetric for AuthenticationUnsuccessfulCount +impl super::AuthEventMetric for AuthenticationSuccessCount where - T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, PrimitiveDateTime: ToSql, AnalyticsCollection: ToSql, Granularity: GroupByClause, @@ -29,19 +27,13 @@ where { async fn load_metrics( &self, - dimensions: &[SdkEventDimensions], + _merchant_id: &str, publishable_key: &str, - filters: &SdkEventFilters, granularity: &Option, time_range: &TimeRange, pool: &T, - ) -> MetricsResult> { + ) -> MetricsResult> { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); - let dimensions = dimensions.to_vec(); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } query_builder .add_select_column(Aggregate::Count { @@ -56,22 +48,24 @@ where .switch()?; } - filters.set_filter_clause(&mut query_builder).switch()?; - query_builder .add_filter_clause("merchant_id", publishable_key) .switch()?; + query_builder + .add_bool_filter_clause("first_event", 1) + .switch()?; + query_builder .add_filter_clause("event_name", SdkEventNames::AuthenticationCall) .switch()?; query_builder - .add_filter_clause("log_type", "ERROR") + .add_filter_clause("log_type", "INFO") .switch()?; query_builder - .add_filter_clause("category", "USER_EVENT") + .add_filter_clause("category", "API") .switch()?; time_range @@ -79,13 +73,6 @@ where .attach_printable("Error filtering time range") .switch()?; - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - if let Some(_granularity) = granularity.as_ref() { query_builder .add_group_by_clause("time_bucket") @@ -94,27 +81,19 @@ where } query_builder - .execute_query::(pool) + .execute_query::(pool) .await .change_context(MetricsError::QueryBuildingError)? .change_context(MetricsError::QueryExecutionFailure)? .into_iter() .map(|i| { Ok(( - SdkEventMetricsBucketIdentifier::new( - i.payment_method.clone(), - i.platform.clone(), - i.browser_name.clone(), - i.source.clone(), - i.component.clone(), - i.payment_experience.clone(), - i.time_bucket.clone(), - ), + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), i, )) }) .collect::, + Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) diff --git a/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs b/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs new file mode 100644 index 0000000000..dbea5a1dc1 --- /dev/null +++ b/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs @@ -0,0 +1,90 @@ +use api_models::analytics::{ + auth_events::{AuthEventFlows, AuthEventMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::AuthEventMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct ChallengeAttemptCount; + +#[async_trait::async_trait] +impl super::AuthEventMetric for ChallengeAttemptCount +where + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + merchant_id: &str, + _publishable_key: &str, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::ConnectorEventsAnalytics); + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + if let Some(granularity) = granularity.as_ref() { + query_builder + .add_granularity_in_mins(granularity) + .switch()?; + } + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder + .add_filter_clause("flow", AuthEventFlows::PostAuthentication) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + if let Some(_granularity) = granularity.as_ref() { + query_builder + .add_group_by_clause("time_bucket") + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/sdk_events/metrics/three_ds_challenge_flow_count.rs b/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs similarity index 63% rename from crates/analytics/src/sdk_events/metrics/three_ds_challenge_flow_count.rs rename to crates/analytics/src/auth_events/metrics/challenge_flow_count.rs index eb2eab6046..51a3b8ed88 100644 --- a/crates/analytics/src/sdk_events/metrics/three_ds_challenge_flow_count.rs +++ b/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs @@ -1,26 +1,24 @@ use api_models::analytics::{ - sdk_events::{ - SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, - }, - Granularity, TimeRange, + auth_events::AuthEventMetricsBucketIdentifier, sdk_events::SdkEventNames, Granularity, + TimeRange, }; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; -use super::SdkEventMetricRow; +use super::AuthEventMetricRow; use crate::{ query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] -pub(super) struct ThreeDsChallengeFlowCount; +pub(super) struct ChallengeFlowCount; #[async_trait::async_trait] -impl super::SdkEventMetric for ThreeDsChallengeFlowCount +impl super::AuthEventMetric for ChallengeFlowCount where - T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, PrimitiveDateTime: ToSql, AnalyticsCollection: ToSql, Granularity: GroupByClause, @@ -29,19 +27,13 @@ where { async fn load_metrics( &self, - dimensions: &[SdkEventDimensions], + _merchant_id: &str, publishable_key: &str, - filters: &SdkEventFilters, granularity: &Option, time_range: &TimeRange, pool: &T, - ) -> MetricsResult> { + ) -> MetricsResult> { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); - let dimensions = dimensions.to_vec(); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } query_builder .add_select_column(Aggregate::Count { @@ -56,14 +48,16 @@ where .switch()?; } - filters.set_filter_clause(&mut query_builder).switch()?; - query_builder .add_filter_clause("merchant_id", publishable_key) .switch()?; query_builder - .add_filter_clause("event_name", SdkEventNames::DisplayThreeDsSdk) + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("category", "USER_EVENT") .switch()?; query_builder @@ -71,7 +65,7 @@ where .switch()?; query_builder - .add_filter_clause("category", "USER_EVENT") + .add_filter_clause("event_name", SdkEventNames::DisplayThreeDsSdk) .switch()?; query_builder.add_filter_clause("value", "C").switch()?; @@ -81,13 +75,6 @@ where .attach_printable("Error filtering time range") .switch()?; - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - if let Some(_granularity) = granularity.as_ref() { query_builder .add_group_by_clause("time_bucket") @@ -96,27 +83,19 @@ where } query_builder - .execute_query::(pool) + .execute_query::(pool) .await .change_context(MetricsError::QueryBuildingError)? .change_context(MetricsError::QueryExecutionFailure)? .into_iter() .map(|i| { Ok(( - SdkEventMetricsBucketIdentifier::new( - i.payment_method.clone(), - i.platform.clone(), - i.browser_name.clone(), - i.source.clone(), - i.component.clone(), - i.payment_experience.clone(), - i.time_bucket.clone(), - ), + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), i, )) }) .collect::, + Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) diff --git a/crates/analytics/src/sdk_events/metrics/three_ds_method_unsuccessful_count.rs b/crates/analytics/src/auth_events/metrics/challenge_success_count.rs similarity index 52% rename from crates/analytics/src/sdk_events/metrics/three_ds_method_unsuccessful_count.rs rename to crates/analytics/src/auth_events/metrics/challenge_success_count.rs index b18d025bd0..cb0932c33c 100644 --- a/crates/analytics/src/sdk_events/metrics/three_ds_method_unsuccessful_count.rs +++ b/crates/analytics/src/auth_events/metrics/challenge_success_count.rs @@ -1,26 +1,24 @@ use api_models::analytics::{ - sdk_events::{ - SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, - }, + auth_events::{AuthEventFlows, AuthEventMetricsBucketIdentifier}, Granularity, TimeRange, }; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; -use super::SdkEventMetricRow; +use super::AuthEventMetricRow; use crate::{ query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] -pub(super) struct ThreeDsMethodUnsuccessfulCount; +pub(super) struct ChallengeSuccessCount; #[async_trait::async_trait] -impl super::SdkEventMetric for ThreeDsMethodUnsuccessfulCount +impl super::AuthEventMetric for ChallengeSuccessCount where - T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, PrimitiveDateTime: ToSql, AnalyticsCollection: ToSql, Granularity: GroupByClause, @@ -29,19 +27,14 @@ where { async fn load_metrics( &self, - dimensions: &[SdkEventDimensions], - publishable_key: &str, - filters: &SdkEventFilters, + merchant_id: &str, + _publishable_key: &str, granularity: &Option, time_range: &TimeRange, pool: &T, - ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); - let dimensions = dimensions.to_vec(); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::ConnectorEventsAnalytics); query_builder .add_select_column(Aggregate::Count { @@ -56,22 +49,16 @@ where .switch()?; } - filters.set_filter_clause(&mut query_builder).switch()?; - query_builder - .add_filter_clause("merchant_id", publishable_key) + .add_filter_clause("merchant_id", merchant_id) .switch()?; query_builder - .add_filter_clause("event_name", SdkEventNames::ThreeDsMethodResult) + .add_filter_clause("flow", AuthEventFlows::PostAuthentication) .switch()?; query_builder - .add_filter_clause("log_type", "ERROR") - .switch()?; - - query_builder - .add_filter_clause("category", "USER_EVENT") + .add_filter_clause("visitParamExtractRaw(response, 'transStatus')", "\"Y\"") .switch()?; time_range @@ -79,13 +66,6 @@ where .attach_printable("Error filtering time range") .switch()?; - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - if let Some(_granularity) = granularity.as_ref() { query_builder .add_group_by_clause("time_bucket") @@ -94,27 +74,19 @@ where } query_builder - .execute_query::(pool) + .execute_query::(pool) .await .change_context(MetricsError::QueryBuildingError)? .change_context(MetricsError::QueryExecutionFailure)? .into_iter() .map(|i| { Ok(( - SdkEventMetricsBucketIdentifier::new( - i.payment_method.clone(), - i.platform.clone(), - i.browser_name.clone(), - i.source.clone(), - i.component.clone(), - i.payment_experience.clone(), - i.time_bucket.clone(), - ), + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), i, )) }) .collect::, + Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) diff --git a/crates/analytics/src/sdk_events/metrics/three_ds_method_skipped_count.rs b/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs similarity index 59% rename from crates/analytics/src/sdk_events/metrics/three_ds_method_skipped_count.rs rename to crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs index 9ffb2a1c69..b5760c9dfa 100644 --- a/crates/analytics/src/sdk_events/metrics/three_ds_method_skipped_count.rs +++ b/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs @@ -1,26 +1,24 @@ use api_models::analytics::{ - sdk_events::{ - SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, - }, - Granularity, TimeRange, + auth_events::AuthEventMetricsBucketIdentifier, sdk_events::SdkEventNames, Granularity, + TimeRange, }; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; -use super::SdkEventMetricRow; +use super::AuthEventMetricRow; use crate::{ query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] -pub(super) struct ThreeDsMethodSkippedCount; +pub(super) struct FrictionlessFlowCount; #[async_trait::async_trait] -impl super::SdkEventMetric for ThreeDsMethodSkippedCount +impl super::AuthEventMetric for FrictionlessFlowCount where - T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, PrimitiveDateTime: ToSql, AnalyticsCollection: ToSql, Granularity: GroupByClause, @@ -29,19 +27,13 @@ where { async fn load_metrics( &self, - dimensions: &[SdkEventDimensions], + _merchant_id: &str, publishable_key: &str, - filters: &SdkEventFilters, granularity: &Option, time_range: &TimeRange, pool: &T, - ) -> MetricsResult> { + ) -> MetricsResult> { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); - let dimensions = dimensions.to_vec(); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } query_builder .add_select_column(Aggregate::Count { @@ -56,14 +48,16 @@ where .switch()?; } - filters.set_filter_clause(&mut query_builder).switch()?; - query_builder .add_filter_clause("merchant_id", publishable_key) .switch()?; query_builder - .add_filter_clause("event_name", SdkEventNames::ThreeDsMethod) + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("category", "USER_EVENT") .switch()?; query_builder @@ -71,23 +65,18 @@ where .switch()?; query_builder - .add_filter_clause("category", "USER_EVENT") + .add_filter_clause("event_name", SdkEventNames::DisplayThreeDsSdk) .switch()?; - query_builder.add_filter_clause("value", "N").switch()?; + query_builder + .add_negative_filter_clause("value", "C") + .switch()?; time_range .set_filter_clause(&mut query_builder) .attach_printable("Error filtering time range") .switch()?; - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - if let Some(_granularity) = granularity.as_ref() { query_builder .add_group_by_clause("time_bucket") @@ -96,27 +85,19 @@ where } query_builder - .execute_query::(pool) + .execute_query::(pool) .await .change_context(MetricsError::QueryBuildingError)? .change_context(MetricsError::QueryExecutionFailure)? .into_iter() .map(|i| { Ok(( - SdkEventMetricsBucketIdentifier::new( - i.payment_method.clone(), - i.platform.clone(), - i.browser_name.clone(), - i.source.clone(), - i.component.clone(), - i.payment_experience.clone(), - i.time_bucket.clone(), - ), + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), i, )) }) .collect::, + Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) diff --git a/crates/analytics/src/sdk_events/metrics/three_ds_method_successful_count.rs b/crates/analytics/src/auth_events/metrics/three_ds_sdk_count.rs similarity index 62% rename from crates/analytics/src/sdk_events/metrics/three_ds_method_successful_count.rs rename to crates/analytics/src/auth_events/metrics/three_ds_sdk_count.rs index 5a8305e8ab..be9d7ab0dc 100644 --- a/crates/analytics/src/sdk_events/metrics/three_ds_method_successful_count.rs +++ b/crates/analytics/src/auth_events/metrics/three_ds_sdk_count.rs @@ -1,26 +1,24 @@ use api_models::analytics::{ - sdk_events::{ - SdkEventDimensions, SdkEventFilters, SdkEventMetricsBucketIdentifier, SdkEventNames, - }, - Granularity, TimeRange, + auth_events::AuthEventMetricsBucketIdentifier, sdk_events::SdkEventNames, Granularity, + TimeRange, }; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; -use super::SdkEventMetricRow; +use super::AuthEventMetricRow; use crate::{ query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; #[derive(Default)] -pub(super) struct ThreeDsMethodSuccessfulCount; +pub(super) struct ThreeDsSdkCount; #[async_trait::async_trait] -impl super::SdkEventMetric for ThreeDsMethodSuccessfulCount +impl super::AuthEventMetric for ThreeDsSdkCount where - T: AnalyticsDataSource + super::SdkEventMetricAnalytics, + T: AnalyticsDataSource + super::AuthEventMetricAnalytics, PrimitiveDateTime: ToSql, AnalyticsCollection: ToSql, Granularity: GroupByClause, @@ -29,19 +27,13 @@ where { async fn load_metrics( &self, - dimensions: &[SdkEventDimensions], + _merchant_id: &str, publishable_key: &str, - filters: &SdkEventFilters, granularity: &Option, time_range: &TimeRange, pool: &T, - ) -> MetricsResult> { + ) -> MetricsResult> { let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::SdkEvents); - let dimensions = dimensions.to_vec(); - - for dim in dimensions.iter() { - query_builder.add_select_column(dim).switch()?; - } query_builder .add_select_column(Aggregate::Count { @@ -56,14 +48,16 @@ where .switch()?; } - filters.set_filter_clause(&mut query_builder).switch()?; - query_builder .add_filter_clause("merchant_id", publishable_key) .switch()?; query_builder - .add_filter_clause("event_name", SdkEventNames::ThreeDsMethodResult) + .add_bool_filter_clause("first_event", 1) + .switch()?; + + query_builder + .add_filter_clause("category", "USER_EVENT") .switch()?; query_builder @@ -71,7 +65,7 @@ where .switch()?; query_builder - .add_filter_clause("category", "USER_EVENT") + .add_filter_clause("event_name", SdkEventNames::ThreeDsMethod) .switch()?; time_range @@ -79,13 +73,6 @@ where .attach_printable("Error filtering time range") .switch()?; - for dim in dimensions.iter() { - query_builder - .add_group_by_clause(dim) - .attach_printable("Error grouping by dimensions") - .switch()?; - } - if let Some(_granularity) = granularity.as_ref() { query_builder .add_group_by_clause("time_bucket") @@ -94,27 +81,19 @@ where } query_builder - .execute_query::(pool) + .execute_query::(pool) .await .change_context(MetricsError::QueryBuildingError)? .change_context(MetricsError::QueryExecutionFailure)? .into_iter() .map(|i| { Ok(( - SdkEventMetricsBucketIdentifier::new( - i.payment_method.clone(), - i.platform.clone(), - i.browser_name.clone(), - i.source.clone(), - i.component.clone(), - i.payment_experience.clone(), - i.time_bucket.clone(), - ), + AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), i, )) }) .collect::, + Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, crate::query::PostProcessingError, >>() .change_context(MetricsError::PostProcessingFailure) diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index 8f0d00cd79..3009c036b9 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -7,6 +7,7 @@ use router_env::logger; use time::PrimitiveDateTime; use super::{ + auth_events::metrics::AuthEventMetricRow, health_check::HealthCheck, payments::{ distribution::PaymentDistributionRow, filters::FilterRow, metrics::PaymentMetricRow, @@ -132,10 +133,11 @@ impl AnalyticsDataSource for ClickhouseClient { | AnalyticsCollection::Dispute => { TableEngine::CollapsingMergeTree { sign: "sign_flag" } } - AnalyticsCollection::SdkEvents => TableEngine::BasicTree, - AnalyticsCollection::ApiEvents => TableEngine::BasicTree, - AnalyticsCollection::ConnectorEvents => TableEngine::BasicTree, - AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, + AnalyticsCollection::SdkEvents + | AnalyticsCollection::ApiEvents + | AnalyticsCollection::ConnectorEvents + | AnalyticsCollection::ConnectorEventsAnalytics + | AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, } } } @@ -158,6 +160,7 @@ impl super::refunds::filters::RefundFilterAnalytics for ClickhouseClient {} impl super::sdk_events::filters::SdkEventFilterAnalytics for ClickhouseClient {} impl super::sdk_events::metrics::SdkEventMetricAnalytics for ClickhouseClient {} impl super::sdk_events::events::SdkEventsFilterAnalytics for ClickhouseClient {} +impl super::auth_events::metrics::AuthEventMetricAnalytics for ClickhouseClient {} impl super::api_event::events::ApiLogsFilterAnalytics for ClickhouseClient {} impl super::api_event::filters::ApiEventFilterAnalytics for ClickhouseClient {} impl super::api_event::metrics::ApiEventMetricAnalytics for ClickhouseClient {} @@ -320,6 +323,16 @@ impl TryInto for serde_json::Value { } } +impl TryInto for serde_json::Value { + type Error = Report; + + fn try_into(self) -> Result { + serde_json::from_value(self).change_context(ParsingError::StructParseFailure( + "Failed to parse AuthEventMetricRow in clickhouse results", + )) + } +} + impl TryInto for serde_json::Value { type Error = Report; @@ -361,6 +374,7 @@ impl ToSql for AnalyticsCollection { Self::Refund => Ok("refunds".to_string()), Self::SdkEvents => Ok("sdk_events_audit".to_string()), Self::ApiEvents => Ok("api_events_audit".to_string()), + Self::ConnectorEventsAnalytics => Ok("connector_events".to_string()), Self::PaymentIntent => Ok("payment_intents".to_string()), Self::ConnectorEvents => Ok("connector_events_audit".to_string()), Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), @@ -423,6 +437,20 @@ where alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } + Self::Percentile { + field, + alias, + percentile, + } => { + format!( + "quantilesExact(0.{})({})[1]{}", + percentile.map_or_else(|| "50".to_owned(), |percentile| percentile.to_string()), + field + .to_sql(table_engine) + .attach_printable("Failed to percentile aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } }) } } diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs index 6ccf2858e2..f327834974 100644 --- a/crates/analytics/src/core.rs +++ b/crates/analytics/src/core.rs @@ -21,6 +21,11 @@ pub async fn get_domain_info( download_dimensions: None, dimensions: utils::get_sdk_event_dimensions(), }, + AnalyticsDomain::AuthEvents => GetInfoResponse { + metrics: utils::get_auth_event_metrics_info(), + download_dimensions: None, + dimensions: Vec::new(), + }, AnalyticsDomain::ApiEvents => GetInfoResponse { metrics: utils::get_api_event_metrics_info(), download_dimensions: None, diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index ae10fc9889..1deca1e075 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -8,6 +8,7 @@ mod query; pub mod refunds; pub mod api_event; +pub mod auth_events; pub mod connector_events; pub mod health_check; pub mod opensearch; @@ -34,6 +35,7 @@ use api_models::analytics::{ api_event::{ ApiEventDimensions, ApiEventFilters, ApiEventMetrics, ApiEventMetricsBucketIdentifier, }, + auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier}, disputes::{DisputeDimensions, DisputeFilters, DisputeMetrics, DisputeMetricsBucketIdentifier}, payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, @@ -54,6 +56,7 @@ use storage_impl::config::Database; use strum::Display; use self::{ + auth_events::metrics::{AuthEventMetric, AuthEventMetricRow}, payments::{ distribution::{PaymentDistribution, PaymentDistributionRow}, metrics::{PaymentMetric, PaymentMetricRow}, @@ -539,6 +542,36 @@ impl AnalyticsProvider { } } + pub async fn get_auth_event_metrics( + &self, + metric: &AuthEventMetrics, + merchant_id: &str, + publishable_key: &str, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(_pool) => Err(report!(MetricsError::NotImplemented)), + Self::Clickhouse(pool) => { + metric + .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) + .await + } + Self::CombinedCkh(_sqlx_pool, ckh_pool) | Self::CombinedSqlx(_sqlx_pool, ckh_pool) => { + metric + .load_metrics( + merchant_id, + publishable_key, + granularity, + // Since API events are ckh only use ckh here + time_range, + ckh_pool, + ) + .await + } + } + } + pub async fn get_api_event_metrics( &self, metric: &ApiEventMetrics, @@ -681,6 +714,7 @@ pub enum AnalyticsFlow { GetPaymentMetrics, GetRefundsMetrics, GetSdkMetrics, + GetAuthMetrics, GetPaymentFilters, GetRefundFilters, GetSdkEventFilters, diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index 525bc0f846..9a0d4ec62c 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -4,6 +4,7 @@ use api_models::{ analytics::{ self as analytics_api, api_event::ApiEventDimensions, + auth_events::AuthEventFlows, disputes::DisputeDimensions, payments::{PaymentDimensions, PaymentDistributions}, refunds::{RefundDimensions, RefundType}, @@ -247,6 +248,11 @@ pub enum Aggregate { field: R, alias: Option<&'static str>, }, + Percentile { + field: R, + alias: Option<&'static str>, + percentile: Option<&'static u8>, + }, } // Window functions in query @@ -379,11 +385,17 @@ impl_to_sql_for_to_string!( Order ); -impl_to_sql_for_to_string!(&SdkEventDimensions, SdkEventDimensions, SdkEventNames); - -impl_to_sql_for_to_string!(&ApiEventDimensions, ApiEventDimensions); - -impl_to_sql_for_to_string!(&DisputeDimensions, DisputeDimensions, DisputeStage); +impl_to_sql_for_to_string!( + &SdkEventDimensions, + SdkEventDimensions, + SdkEventNames, + AuthEventFlows, + &ApiEventDimensions, + ApiEventDimensions, + &DisputeDimensions, + DisputeDimensions, + DisputeStage +); #[derive(Debug)] pub enum FilterTypes { @@ -507,6 +519,14 @@ where self.add_custom_filter_clause(key, value, FilterTypes::EqualBool) } + pub fn add_negative_filter_clause( + &mut self, + key: impl ToSql, + value: impl ToSql, + ) -> QueryResult<()> { + self.add_custom_filter_clause(key, value, FilterTypes::NotEqual) + } + pub fn add_custom_filter_clause( &mut self, lhs: impl ToSql, diff --git a/crates/analytics/src/sdk_events/accumulator.rs b/crates/analytics/src/sdk_events/accumulator.rs index fc90ae48eb..d6b6ea0e83 100644 --- a/crates/analytics/src/sdk_events/accumulator.rs +++ b/crates/analytics/src/sdk_events/accumulator.rs @@ -7,18 +7,12 @@ use super::metrics::SdkEventMetricRow; pub struct SdkEventMetricsAccumulator { pub payment_attempts: CountAccumulator, pub payment_methods_call_count: CountAccumulator, - pub average_payment_time: AverageAccumulator, + pub average_payment_time: CountAccumulator, + pub load_time: CountAccumulator, pub sdk_initiated_count: CountAccumulator, pub sdk_rendered_count: CountAccumulator, pub payment_method_selected_count: CountAccumulator, pub payment_data_filled_count: CountAccumulator, - pub three_ds_method_invoked_count: CountAccumulator, - pub three_ds_method_skipped_count: CountAccumulator, - pub three_ds_method_successful_count: CountAccumulator, - pub three_ds_method_unsuccessful_count: CountAccumulator, - pub authentication_unsuccessful_count: CountAccumulator, - pub three_ds_challenge_flow_count: CountAccumulator, - pub three_ds_frictionless_flow_count: CountAccumulator, } #[derive(Debug, Default)] @@ -94,17 +88,11 @@ impl SdkEventMetricsAccumulator { payment_attempts: self.payment_attempts.collect(), payment_methods_call_count: self.payment_methods_call_count.collect(), average_payment_time: self.average_payment_time.collect(), + load_time: self.load_time.collect(), sdk_initiated_count: self.sdk_initiated_count.collect(), sdk_rendered_count: self.sdk_rendered_count.collect(), payment_method_selected_count: self.payment_method_selected_count.collect(), payment_data_filled_count: self.payment_data_filled_count.collect(), - three_ds_method_invoked_count: self.three_ds_method_invoked_count.collect(), - three_ds_method_skipped_count: self.three_ds_method_skipped_count.collect(), - three_ds_method_successful_count: self.three_ds_method_successful_count.collect(), - three_ds_method_unsuccessful_count: self.three_ds_method_unsuccessful_count.collect(), - authentication_unsuccessful_count: self.authentication_unsuccessful_count.collect(), - three_ds_challenge_flow_count: self.three_ds_challenge_flow_count.collect(), - three_ds_frictionless_flow_count: self.three_ds_frictionless_flow_count.collect(), } } } diff --git a/crates/analytics/src/sdk_events/core.rs b/crates/analytics/src/sdk_events/core.rs index 2ffc7f1aed..1dcf4143aa 100644 --- a/crates/analytics/src/sdk_events/core.rs +++ b/crates/analytics/src/sdk_events/core.rs @@ -106,27 +106,9 @@ pub async fn get_metrics( SdkEventMetrics::AveragePaymentTime => metrics_builder .average_payment_time .add_metrics_bucket(&value), - SdkEventMetrics::ThreeDsMethodInvokedCount => metrics_builder - .three_ds_method_invoked_count - .add_metrics_bucket(&value), - SdkEventMetrics::ThreeDsMethodSkippedCount => metrics_builder - .three_ds_method_skipped_count - .add_metrics_bucket(&value), - SdkEventMetrics::ThreeDsMethodSuccessfulCount => metrics_builder - .three_ds_method_successful_count - .add_metrics_bucket(&value), - SdkEventMetrics::ThreeDsMethodUnsuccessfulCount => metrics_builder - .three_ds_method_unsuccessful_count - .add_metrics_bucket(&value), - SdkEventMetrics::AuthenticationUnsuccessfulCount => metrics_builder - .authentication_unsuccessful_count - .add_metrics_bucket(&value), - SdkEventMetrics::ThreeDsChallengeFlowCount => metrics_builder - .three_ds_challenge_flow_count - .add_metrics_bucket(&value), - SdkEventMetrics::ThreeDsFrictionlessFlowCount => metrics_builder - .three_ds_frictionless_flow_count - .add_metrics_bucket(&value), + SdkEventMetrics::LoadTime => { + metrics_builder.load_time.add_metrics_bucket(&value) + } } } diff --git a/crates/analytics/src/sdk_events/metrics.rs b/crates/analytics/src/sdk_events/metrics.rs index 70d29a2eb6..50481084e4 100644 --- a/crates/analytics/src/sdk_events/metrics.rs +++ b/crates/analytics/src/sdk_events/metrics.rs @@ -11,35 +11,23 @@ use crate::{ types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, }; -mod authentication_unsuccessful_count; mod average_payment_time; +mod load_time; mod payment_attempts; mod payment_data_filled_count; mod payment_method_selected_count; mod payment_methods_call_count; mod sdk_initiated_count; mod sdk_rendered_count; -mod three_ds_challenge_flow_count; -mod three_ds_frictionless_flow_count; -mod three_ds_method_invoked_count; -mod three_ds_method_skipped_count; -mod three_ds_method_successful_count; -mod three_ds_method_unsuccessful_count; -use authentication_unsuccessful_count::AuthenticationUnsuccessfulCount; use average_payment_time::AveragePaymentTime; +use load_time::LoadTime; use payment_attempts::PaymentAttempts; use payment_data_filled_count::PaymentDataFilledCount; use payment_method_selected_count::PaymentMethodSelectedCount; use payment_methods_call_count::PaymentMethodsCallCount; use sdk_initiated_count::SdkInitiatedCount; use sdk_rendered_count::SdkRenderedCount; -use three_ds_challenge_flow_count::ThreeDsChallengeFlowCount; -use three_ds_frictionless_flow_count::ThreeDsFrictionlessFlowCount; -use three_ds_method_invoked_count::ThreeDsMethodInvokedCount; -use three_ds_method_skipped_count::ThreeDsMethodSkippedCount; -use three_ds_method_successful_count::ThreeDsMethodSuccessfulCount; -use three_ds_method_unsuccessful_count::ThreeDsMethodUnsuccessfulCount; #[derive(Debug, PartialEq, Eq, serde::Deserialize)] pub struct SdkEventMetricRow { @@ -176,80 +164,8 @@ where ) .await } - Self::ThreeDsMethodSkippedCount => { - ThreeDsMethodSkippedCount - .load_metrics( - dimensions, - publishable_key, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::ThreeDsMethodInvokedCount => { - ThreeDsMethodInvokedCount - .load_metrics( - dimensions, - publishable_key, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::ThreeDsMethodSuccessfulCount => { - ThreeDsMethodSuccessfulCount - .load_metrics( - dimensions, - publishable_key, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::ThreeDsMethodUnsuccessfulCount => { - ThreeDsMethodUnsuccessfulCount - .load_metrics( - dimensions, - publishable_key, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::AuthenticationUnsuccessfulCount => { - AuthenticationUnsuccessfulCount - .load_metrics( - dimensions, - publishable_key, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::ThreeDsChallengeFlowCount => { - ThreeDsChallengeFlowCount - .load_metrics( - dimensions, - publishable_key, - filters, - granularity, - time_range, - pool, - ) - .await - } - Self::ThreeDsFrictionlessFlowCount => { - ThreeDsFrictionlessFlowCount + Self::LoadTime => { + LoadTime .load_metrics( dimensions, publishable_key, diff --git a/crates/analytics/src/sdk_events/metrics/average_payment_time.rs b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs index db7171524a..a81a55a2af 100644 --- a/crates/analytics/src/sdk_events/metrics/average_payment_time.rs +++ b/crates/analytics/src/sdk_events/metrics/average_payment_time.rs @@ -44,16 +44,10 @@ where } query_builder - .add_select_column(Aggregate::Count { - field: None, - alias: Some("count"), - }) - .switch()?; - - query_builder - .add_select_column(Aggregate::Sum { + .add_select_column(Aggregate::Percentile { field: "latency", - alias: Some("total"), + alias: Some("count"), + percentile: Some(&50), }) .switch()?; diff --git a/crates/analytics/src/sdk_events/metrics/three_ds_frictionless_flow_count.rs b/crates/analytics/src/sdk_events/metrics/load_time.rs similarity index 87% rename from crates/analytics/src/sdk_events/metrics/three_ds_frictionless_flow_count.rs rename to crates/analytics/src/sdk_events/metrics/load_time.rs index ec40cf50c2..07168069b5 100644 --- a/crates/analytics/src/sdk_events/metrics/three_ds_frictionless_flow_count.rs +++ b/crates/analytics/src/sdk_events/metrics/load_time.rs @@ -15,10 +15,10 @@ use crate::{ }; #[derive(Default)] -pub(super) struct ThreeDsFrictionlessFlowCount; +pub(super) struct LoadTime; #[async_trait::async_trait] -impl super::SdkEventMetric for ThreeDsFrictionlessFlowCount +impl super::SdkEventMetric for LoadTime where T: AnalyticsDataSource + super::SdkEventMetricAnalytics, PrimitiveDateTime: ToSql, @@ -44,9 +44,10 @@ where } query_builder - .add_select_column(Aggregate::Count { - field: None, + .add_select_column(Aggregate::Percentile { + field: "latency", alias: Some("count"), + percentile: Some(&50), }) .switch()?; @@ -63,19 +64,15 @@ where .switch()?; query_builder - .add_filter_clause("event_name", SdkEventNames::DisplayThreeDsSdk) + .add_bool_filter_clause("first_event", 1) .switch()?; query_builder - .add_filter_clause("log_type", "INFO") + .add_filter_clause("event_name", SdkEventNames::AppRendered) .switch()?; query_builder - .add_filter_clause("category", "USER_EVENT") - .switch()?; - - query_builder - .add_custom_filter_clause("value", "C", FilterTypes::NotEqual) + .add_custom_filter_clause("latency", 0, FilterTypes::Gt) .switch()?; time_range diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 133e959d63..df146cc799 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -515,8 +515,10 @@ impl ToSql for AnalyticsCollection { Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) .attach_printable("ApiEvents table is not implemented for Sqlx"))?, Self::PaymentIntent => Ok("payment_intent".to_string()), - Self::ConnectorEvents => Err(error_stack::report!(ParsingError::UnknownError) - .attach_printable("ConnectorEvents table is not implemented for Sqlx"))?, + Self::ConnectorEvents | Self::ConnectorEventsAnalytics => { + Err(error_stack::report!(ParsingError::UnknownError) + .attach_printable("ConnectorEvents table is not implemented for Sqlx"))? + } Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, Self::Dispute => Ok("dispute".to_string()), @@ -563,6 +565,20 @@ where alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) ) } + Self::Percentile { + field, + alias, + percentile, + } => { + format!( + "percentile_cont(0.{}) within group (order by {} asc){}", + percentile.map_or_else(|| "50".to_owned(), |percentile| percentile.to_string()), + field + .to_sql(table_engine) + .attach_printable("Failed to percentile aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } }) } } diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index d0c1d74bc1..e8196a0968 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -15,6 +15,7 @@ use crate::errors::AnalyticsError; pub enum AnalyticsDomain { Payments, Refunds, + AuthEvents, SdkEvents, ApiEvents, Dispute, @@ -30,6 +31,7 @@ pub enum AnalyticsCollection { ConnectorEvents, OutgoingWebhookEvent, Dispute, + ConnectorEventsAnalytics, } #[allow(dead_code)] diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs index 7bff5c87da..0afe9bd6c5 100644 --- a/crates/analytics/src/utils.rs +++ b/crates/analytics/src/utils.rs @@ -1,5 +1,6 @@ use api_models::analytics::{ api_event::{ApiEventDimensions, ApiEventMetrics}, + auth_events::AuthEventMetrics, disputes::{DisputeDimensions, DisputeMetrics}, payments::{PaymentDimensions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, @@ -36,6 +37,10 @@ pub fn get_sdk_event_metrics_info() -> Vec { SdkEventMetrics::iter().map(Into::into).collect() } +pub fn get_auth_event_metrics_info() -> Vec { + AuthEventMetrics::iter().map(Into::into).collect() +} + pub fn get_api_event_metrics_info() -> Vec { ApiEventMetrics::iter().map(Into::into).collect() } diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index ad035c707c..d697c8ab27 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -5,6 +5,7 @@ use masking::Secret; use self::{ api_event::{ApiEventDimensions, ApiEventMetrics}, + auth_events::AuthEventMetrics, disputes::{DisputeDimensions, DisputeMetrics}, payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, @@ -13,6 +14,7 @@ use self::{ pub use crate::payments::TimeRange; pub mod api_event; +pub mod auth_events; pub mod connector_events; pub mod disputes; pub mod outgoing_webhook_event; @@ -138,6 +140,17 @@ pub struct GetSdkEventMetricRequest { pub delta: bool, } +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetAuthEventMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} + #[derive(Debug, serde::Serialize)] pub struct AnalyticsMetadata { pub current_time_range: TimeRange, diff --git a/crates/api_models/src/analytics/auth_events.rs b/crates/api_models/src/analytics/auth_events.rs new file mode 100644 index 0000000000..bb08f9f593 --- /dev/null +++ b/crates/api_models/src/analytics/auth_events.rs @@ -0,0 +1,111 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::NameDescription; + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum AuthEventMetrics { + ThreeDsSdkCount, + AuthenticationAttemptCount, + AuthenticationSuccessCount, + ChallengeFlowCount, + FrictionlessFlowCount, + ChallengeAttemptCount, + ChallengeSuccessCount, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +pub enum AuthEventFlows { + PostAuthentication, +} + +pub mod metric_behaviour { + pub struct ThreeDsSdkCount; + pub struct AuthenticationAttemptCount; + pub struct AuthenticationSuccessCount; + pub struct ChallengeFlowCount; + pub struct FrictionlessFlowCount; + pub struct ChallengeAttemptCount; + pub struct ChallengeSuccessCount; +} + +impl From for NameDescription { + fn from(value: AuthEventMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct AuthEventMetricsBucketIdentifier { + pub time_bucket: Option, +} + +impl AuthEventMetricsBucketIdentifier { + pub fn new(time_bucket: Option) -> Self { + Self { time_bucket } + } +} + +impl Hash for AuthEventMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.time_bucket.hash(state); + } +} + +impl PartialEq for AuthEventMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct AuthEventMetricsBucketValue { + pub three_ds_sdk_count: Option, + pub authentication_attempt_count: Option, + pub authentication_success_count: Option, + pub challenge_flow_count: Option, + pub challenge_attempt_count: Option, + pub challenge_success_count: Option, + pub frictionless_flow_count: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: AuthEventMetricsBucketValue, + #[serde(flatten)] + pub dimensions: AuthEventMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/analytics/sdk_events.rs b/crates/api_models/src/analytics/sdk_events.rs index 2eacaa655d..9e33181ec6 100644 --- a/crates/api_models/src/analytics/sdk_events.rs +++ b/crates/api_models/src/analytics/sdk_events.rs @@ -72,18 +72,12 @@ pub enum SdkEventDimensions { pub enum SdkEventMetrics { PaymentAttempts, PaymentMethodsCallCount, - ThreeDsMethodInvokedCount, - ThreeDsMethodSkippedCount, - ThreeDsMethodSuccessfulCount, - ThreeDsMethodUnsuccessfulCount, - AuthenticationUnsuccessfulCount, - ThreeDsChallengeFlowCount, - ThreeDsFrictionlessFlowCount, SdkRenderedCount, SdkInitiatedCount, PaymentMethodSelectedCount, PaymentDataFilledCount, AveragePaymentTime, + LoadTime, } #[derive( @@ -114,6 +108,7 @@ pub enum SdkEventNames { DisplayBankTransferInfoPage, DisplayQrCodeInfoPage, AuthenticationCall, + AuthenticationCallInit, ThreeDsMethodCall, ThreeDsMethodResult, ThreeDsMethod, @@ -124,18 +119,12 @@ pub enum SdkEventNames { pub mod metric_behaviour { pub struct PaymentAttempts; pub struct PaymentMethodsCallCount; - pub struct ThreeDsMethodInvokedCount; - pub struct ThreeDsMethodSkippedCount; - pub struct ThreeDsMethodSuccessfulCount; - pub struct ThreeDsMethodUnsuccessfulCount; - pub struct AuthenticationUnsuccessfulCount; - pub struct ThreeDsChallengeFlowCount; - pub struct ThreeDsFrictionlessFlowCount; pub struct SdkRenderedCount; pub struct SdkInitiatedCount; pub struct PaymentMethodSelectedCount; pub struct PaymentDataFilledCount; pub struct AveragePaymentTime; + pub struct LoadTime; } impl From for NameDescription { @@ -215,18 +204,12 @@ impl PartialEq for SdkEventMetricsBucketIdentifier { pub struct SdkEventMetricsBucketValue { pub payment_attempts: Option, pub payment_methods_call_count: Option, - pub average_payment_time: Option, + pub average_payment_time: Option, + pub load_time: Option, pub sdk_rendered_count: Option, pub sdk_initiated_count: Option, pub payment_method_selected_count: Option, pub payment_data_filled_count: Option, - pub three_ds_method_invoked_count: Option, - pub three_ds_method_skipped_count: Option, - pub three_ds_method_successful_count: Option, - pub three_ds_method_unsuccessful_count: Option, - pub authentication_unsuccessful_count: Option, - pub three_ds_challenge_flow_count: Option, - pub three_ds_frictionless_flow_count: Option, } #[derive(Debug, serde::Serialize)] diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 46fa8caa6d..c9ae775fed 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -22,7 +22,7 @@ use common_utils::{ use crate::{ admin::*, analytics::{ - api_event::*, connector_events::ConnectorEventsRequest, + api_event::*, auth_events::*, connector_events::ConnectorEventsRequest, outgoing_webhook_event::OutgoingWebhookLogsRequest, sdk_events::*, search::*, *, }, api_keys::*, @@ -84,6 +84,7 @@ impl_misc_api_event_type!( GetPaymentMetricRequest, GetRefundMetricRequest, GetSdkEventMetricRequest, + GetAuthEventMetricRequest, GetPaymentFiltersRequest, PaymentFiltersResponse, GetRefundFilterRequest, diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 8eb94a2912..0131c08591 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -13,9 +13,9 @@ pub mod routes { GetGlobalSearchRequest, GetSearchRequest, GetSearchRequestWithIndex, SearchIndex, }, GenerateReportRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, - GetDisputeMetricRequest, GetPaymentFiltersRequest, GetPaymentMetricRequest, - GetRefundFilterRequest, GetRefundMetricRequest, GetSdkEventFiltersRequest, - GetSdkEventMetricRequest, ReportRequest, + GetAuthEventMetricRequest, GetDisputeMetricRequest, GetPaymentFiltersRequest, + GetPaymentMetricRequest, GetRefundFilterRequest, GetRefundMetricRequest, + GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, }; use error_stack::ResultExt; @@ -74,6 +74,10 @@ pub mod routes { web::resource("filters/sdk_events") .route(web::post().to(get_sdk_event_filters)), ) + .service( + web::resource("metrics/auth_events") + .route(web::post().to(get_auth_event_metrics)), + ) .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) .service( @@ -241,6 +245,43 @@ pub mod routes { .await } + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetAuthEventMetricRequest` element. + pub async fn get_auth_event_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetAuthEventMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetAuthEventMetricRequest"); + let flow = AnalyticsFlow::GetAuthMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req, _| async move { + analytics::auth_events::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + auth.merchant_account.publishable_key.as_ref(), + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + pub async fn get_payment_filters( state: web::Data, req: actix_web::HttpRequest,