mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +08:00 
			
		
		
		
	feat: authentication analytics (#4684)
This commit is contained in:
		| @ -11,6 +11,7 @@ pub struct AuthEventMetricsAccumulator { | |||||||
|     pub challenge_attempt_count: CountAccumulator, |     pub challenge_attempt_count: CountAccumulator, | ||||||
|     pub challenge_success_count: CountAccumulator, |     pub challenge_success_count: CountAccumulator, | ||||||
|     pub frictionless_flow_count: CountAccumulator, |     pub frictionless_flow_count: CountAccumulator, | ||||||
|  |     pub frictionless_success_count: CountAccumulator, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, Default)] | #[derive(Debug, Default)] | ||||||
| @ -53,6 +54,7 @@ impl AuthEventMetricsAccumulator { | |||||||
|             challenge_attempt_count: self.challenge_attempt_count.collect(), |             challenge_attempt_count: self.challenge_attempt_count.collect(), | ||||||
|             challenge_success_count: self.challenge_success_count.collect(), |             challenge_success_count: self.challenge_success_count.collect(), | ||||||
|             frictionless_flow_count: self.frictionless_flow_count.collect(), |             frictionless_flow_count: self.frictionless_flow_count.collect(), | ||||||
|  |             frictionless_success_count: self.frictionless_success_count.collect(), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -78,6 +78,9 @@ pub async fn get_metrics( | |||||||
|                     AuthEventMetrics::FrictionlessFlowCount => metrics_builder |                     AuthEventMetrics::FrictionlessFlowCount => metrics_builder | ||||||
|                         .frictionless_flow_count |                         .frictionless_flow_count | ||||||
|                         .add_metrics_bucket(&value), |                         .add_metrics_bucket(&value), | ||||||
|  |                     AuthEventMetrics::FrictionlessSuccessCount => metrics_builder | ||||||
|  |                         .frictionless_success_count | ||||||
|  |                         .add_metrics_bucket(&value), | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ mod challenge_attempt_count; | |||||||
| mod challenge_flow_count; | mod challenge_flow_count; | ||||||
| mod challenge_success_count; | mod challenge_success_count; | ||||||
| mod frictionless_flow_count; | mod frictionless_flow_count; | ||||||
|  | mod frictionless_success_count; | ||||||
| mod three_ds_sdk_count; | mod three_ds_sdk_count; | ||||||
|  |  | ||||||
| use authentication_attempt_count::AuthenticationAttemptCount; | use authentication_attempt_count::AuthenticationAttemptCount; | ||||||
| @ -23,6 +24,7 @@ use challenge_attempt_count::ChallengeAttemptCount; | |||||||
| use challenge_flow_count::ChallengeFlowCount; | use challenge_flow_count::ChallengeFlowCount; | ||||||
| use challenge_success_count::ChallengeSuccessCount; | use challenge_success_count::ChallengeSuccessCount; | ||||||
| use frictionless_flow_count::FrictionlessFlowCount; | use frictionless_flow_count::FrictionlessFlowCount; | ||||||
|  | use frictionless_success_count::FrictionlessSuccessCount; | ||||||
| use three_ds_sdk_count::ThreeDsSdkCount; | use three_ds_sdk_count::ThreeDsSdkCount; | ||||||
|  |  | ||||||
| #[derive(Debug, PartialEq, Eq, serde::Deserialize)] | #[derive(Debug, PartialEq, Eq, serde::Deserialize)] | ||||||
| @ -102,6 +104,11 @@ where | |||||||
|                     .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) |                     .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||||||
|                     .await |                     .await | ||||||
|             } |             } | ||||||
|  |             Self::FrictionlessSuccessCount => { | ||||||
|  |                 FrictionlessSuccessCount | ||||||
|  |                     .load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||||||
|  |                     .await | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ use time::PrimitiveDateTime; | |||||||
|  |  | ||||||
| use super::AuthEventMetricRow; | use super::AuthEventMetricRow; | ||||||
| use crate::{ | use crate::{ | ||||||
|     query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, |     query::{Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, | ||||||
|     types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, |     types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @ -34,7 +34,7 @@ where | |||||||
|         pool: &T, |         pool: &T, | ||||||
|     ) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> { |     ) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> { | ||||||
|         let mut query_builder: QueryBuilder<T> = |         let mut query_builder: QueryBuilder<T> = | ||||||
|             QueryBuilder::new(AnalyticsCollection::ConnectorEventsAnalytics); |             QueryBuilder::new(AnalyticsCollection::ApiEventsAnalytics); | ||||||
|  |  | ||||||
|         query_builder |         query_builder | ||||||
|             .add_select_column(Aggregate::Count { |             .add_select_column(Aggregate::Count { | ||||||
| @ -54,7 +54,11 @@ where | |||||||
|             .switch()?; |             .switch()?; | ||||||
|  |  | ||||||
|         query_builder |         query_builder | ||||||
|             .add_filter_clause("flow", AuthEventFlows::PostAuthentication) |             .add_filter_clause("api_flow", AuthEventFlows::IncomingWebhookReceive) | ||||||
|  |             .switch()?; | ||||||
|  |  | ||||||
|  |         query_builder | ||||||
|  |             .add_custom_filter_clause("request", "threeDSServerTransID", FilterTypes::Like) | ||||||
|             .switch()?; |             .switch()?; | ||||||
|  |  | ||||||
|         time_range |         time_range | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ where | |||||||
|         pool: &T, |         pool: &T, | ||||||
|     ) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> { |     ) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> { | ||||||
|         let mut query_builder: QueryBuilder<T> = |         let mut query_builder: QueryBuilder<T> = | ||||||
|             QueryBuilder::new(AnalyticsCollection::ConnectorEventsAnalytics); |             QueryBuilder::new(AnalyticsCollection::ApiEventsAnalytics); | ||||||
|  |  | ||||||
|         query_builder |         query_builder | ||||||
|             .add_select_column(Aggregate::Count { |             .add_select_column(Aggregate::Count { | ||||||
| @ -54,11 +54,11 @@ where | |||||||
|             .switch()?; |             .switch()?; | ||||||
|  |  | ||||||
|         query_builder |         query_builder | ||||||
|             .add_filter_clause("flow", AuthEventFlows::PostAuthentication) |             .add_filter_clause("api_flow", AuthEventFlows::IncomingWebhookReceive) | ||||||
|             .switch()?; |             .switch()?; | ||||||
|  |  | ||||||
|         query_builder |         query_builder | ||||||
|             .add_filter_clause("visitParamExtractRaw(response, 'transStatus')", "\"Y\"") |             .add_filter_clause("visitParamExtractRaw(request, 'transStatus')", "\"Y\"") | ||||||
|             .switch()?; |             .switch()?; | ||||||
|  |  | ||||||
|         time_range |         time_range | ||||||
|  | |||||||
| @ -0,0 +1,94 @@ | |||||||
|  | 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 FrictionlessSuccessCount; | ||||||
|  |  | ||||||
|  | #[async_trait::async_trait] | ||||||
|  | impl<T> super::AuthEventMetric<T> for FrictionlessSuccessCount | ||||||
|  | where | ||||||
|  |     T: AnalyticsDataSource + super::AuthEventMetricAnalytics, | ||||||
|  |     PrimitiveDateTime: ToSql<T>, | ||||||
|  |     AnalyticsCollection: ToSql<T>, | ||||||
|  |     Granularity: GroupByClause<T>, | ||||||
|  |     Aggregate<&'static str>: ToSql<T>, | ||||||
|  |     Window<&'static str>: ToSql<T>, | ||||||
|  | { | ||||||
|  |     async fn load_metrics( | ||||||
|  |         &self, | ||||||
|  |         merchant_id: &str, | ||||||
|  |         _publishable_key: &str, | ||||||
|  |         granularity: &Option<Granularity>, | ||||||
|  |         time_range: &TimeRange, | ||||||
|  |         pool: &T, | ||||||
|  |     ) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> { | ||||||
|  |         let mut query_builder: QueryBuilder<T> = | ||||||
|  |             QueryBuilder::new(AnalyticsCollection::ApiEventsAnalytics); | ||||||
|  |  | ||||||
|  |         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("api_flow", AuthEventFlows::PaymentsExternalAuthentication) | ||||||
|  |             .switch()?; | ||||||
|  |  | ||||||
|  |         query_builder | ||||||
|  |             .add_filter_clause("visitParamExtractRaw(response, 'transStatus')", "\"Y\"") | ||||||
|  |             .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::<AuthEventMetricRow, _>(pool) | ||||||
|  |             .await | ||||||
|  |             .change_context(MetricsError::QueryBuildingError)? | ||||||
|  |             .change_context(MetricsError::QueryExecutionFailure)? | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|i| { | ||||||
|  |                 Ok(( | ||||||
|  |                     AuthEventMetricsBucketIdentifier::new(i.time_bucket.clone()), | ||||||
|  |                     i, | ||||||
|  |                 )) | ||||||
|  |             }) | ||||||
|  |             .collect::<error_stack::Result< | ||||||
|  |                 Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>, | ||||||
|  |                 crate::query::PostProcessingError, | ||||||
|  |             >>() | ||||||
|  |             .change_context(MetricsError::PostProcessingFailure) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -136,7 +136,7 @@ impl AnalyticsDataSource for ClickhouseClient { | |||||||
|             AnalyticsCollection::SdkEvents |             AnalyticsCollection::SdkEvents | ||||||
|             | AnalyticsCollection::ApiEvents |             | AnalyticsCollection::ApiEvents | ||||||
|             | AnalyticsCollection::ConnectorEvents |             | AnalyticsCollection::ConnectorEvents | ||||||
|             | AnalyticsCollection::ConnectorEventsAnalytics |             | AnalyticsCollection::ApiEventsAnalytics | ||||||
|             | AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, |             | AnalyticsCollection::OutgoingWebhookEvent => TableEngine::BasicTree, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @ -374,7 +374,7 @@ impl ToSql<ClickhouseClient> for AnalyticsCollection { | |||||||
|             Self::Refund => Ok("refunds".to_string()), |             Self::Refund => Ok("refunds".to_string()), | ||||||
|             Self::SdkEvents => Ok("sdk_events_audit".to_string()), |             Self::SdkEvents => Ok("sdk_events_audit".to_string()), | ||||||
|             Self::ApiEvents => Ok("api_events_audit".to_string()), |             Self::ApiEvents => Ok("api_events_audit".to_string()), | ||||||
|             Self::ConnectorEventsAnalytics => Ok("connector_events".to_string()), |             Self::ApiEventsAnalytics => Ok("api_events".to_string()), | ||||||
|             Self::PaymentIntent => Ok("payment_intents".to_string()), |             Self::PaymentIntent => Ok("payment_intents".to_string()), | ||||||
|             Self::ConnectorEvents => Ok("connector_events_audit".to_string()), |             Self::ConnectorEvents => Ok("connector_events_audit".to_string()), | ||||||
|             Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), |             Self::OutgoingWebhookEvent => Ok("outgoing_webhook_events_audit".to_string()), | ||||||
|  | |||||||
| @ -548,10 +548,10 @@ impl ToSql<SqlxClient> for AnalyticsCollection { | |||||||
|             Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) |             Self::ApiEvents => Err(error_stack::report!(ParsingError::UnknownError) | ||||||
|                 .attach_printable("ApiEvents table is not implemented for Sqlx"))?, |                 .attach_printable("ApiEvents table is not implemented for Sqlx"))?, | ||||||
|             Self::PaymentIntent => Ok("payment_intent".to_string()), |             Self::PaymentIntent => Ok("payment_intent".to_string()), | ||||||
|             Self::ConnectorEvents | Self::ConnectorEventsAnalytics => { |             Self::ConnectorEvents => Err(error_stack::report!(ParsingError::UnknownError) | ||||||
|                 Err(error_stack::report!(ParsingError::UnknownError) |                 .attach_printable("ConnectorEvents table is not implemented for Sqlx"))?, | ||||||
|                     .attach_printable("ConnectorEvents table is not implemented for Sqlx"))? |             Self::ApiEventsAnalytics => Err(error_stack::report!(ParsingError::UnknownError) | ||||||
|             } |                 .attach_printable("ApiEvents table is not implemented for Sqlx"))?, | ||||||
|             Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) |             Self::OutgoingWebhookEvent => Err(error_stack::report!(ParsingError::UnknownError) | ||||||
|                 .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, |                 .attach_printable("OutgoingWebhookEvents table is not implemented for Sqlx"))?, | ||||||
|             Self::Dispute => Ok("dispute".to_string()), |             Self::Dispute => Ok("dispute".to_string()), | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ pub enum AnalyticsCollection { | |||||||
|     ConnectorEvents, |     ConnectorEvents, | ||||||
|     OutgoingWebhookEvent, |     OutgoingWebhookEvent, | ||||||
|     Dispute, |     Dispute, | ||||||
|     ConnectorEventsAnalytics, |     ApiEventsAnalytics, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[allow(dead_code)] | #[allow(dead_code)] | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ pub enum AuthEventMetrics { | |||||||
|     AuthenticationSuccessCount, |     AuthenticationSuccessCount, | ||||||
|     ChallengeFlowCount, |     ChallengeFlowCount, | ||||||
|     FrictionlessFlowCount, |     FrictionlessFlowCount, | ||||||
|  |     FrictionlessSuccessCount, | ||||||
|     ChallengeAttemptCount, |     ChallengeAttemptCount, | ||||||
|     ChallengeSuccessCount, |     ChallengeSuccessCount, | ||||||
| } | } | ||||||
| @ -42,7 +43,8 @@ pub enum AuthEventMetrics { | |||||||
|     strum::AsRefStr, |     strum::AsRefStr, | ||||||
| )] | )] | ||||||
| pub enum AuthEventFlows { | pub enum AuthEventFlows { | ||||||
|     PostAuthentication, |     IncomingWebhookReceive, | ||||||
|  |     PaymentsExternalAuthentication, | ||||||
| } | } | ||||||
|  |  | ||||||
| pub mod metric_behaviour { | pub mod metric_behaviour { | ||||||
| @ -51,6 +53,7 @@ pub mod metric_behaviour { | |||||||
|     pub struct AuthenticationSuccessCount; |     pub struct AuthenticationSuccessCount; | ||||||
|     pub struct ChallengeFlowCount; |     pub struct ChallengeFlowCount; | ||||||
|     pub struct FrictionlessFlowCount; |     pub struct FrictionlessFlowCount; | ||||||
|  |     pub struct FrictionlessSuccessCount; | ||||||
|     pub struct ChallengeAttemptCount; |     pub struct ChallengeAttemptCount; | ||||||
|     pub struct ChallengeSuccessCount; |     pub struct ChallengeSuccessCount; | ||||||
| } | } | ||||||
| @ -100,6 +103,7 @@ pub struct AuthEventMetricsBucketValue { | |||||||
|     pub challenge_attempt_count: Option<u64>, |     pub challenge_attempt_count: Option<u64>, | ||||||
|     pub challenge_success_count: Option<u64>, |     pub challenge_success_count: Option<u64>, | ||||||
|     pub frictionless_flow_count: Option<u64>, |     pub frictionless_flow_count: Option<u64>, | ||||||
|  |     pub frictionless_success_count: Option<u64>, | ||||||
| } | } | ||||||
|  |  | ||||||
| #[derive(Debug, serde::Serialize)] | #[derive(Debug, serde::Serialize)] | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Vrishab Srivatsa
					Vrishab Srivatsa