diff --git a/crates/analytics/src/auth_events.rs b/crates/analytics/src/auth_events.rs index 3aa23d0793..2ce5d52f9b 100644 --- a/crates/analytics/src/auth_events.rs +++ b/crates/analytics/src/auth_events.rs @@ -2,7 +2,8 @@ pub mod accumulator; mod core; pub mod filters; pub mod metrics; +pub mod sankey; pub mod types; pub use accumulator::{AuthEventMetricAccumulator, AuthEventMetricsAccumulator}; -pub use self::core::{get_filters, get_metrics}; +pub use self::core::{get_filters, get_metrics, get_sankey}; diff --git a/crates/analytics/src/auth_events/accumulator.rs b/crates/analytics/src/auth_events/accumulator.rs index 13818d2bd4..2ee5d242d6 100644 --- a/crates/analytics/src/auth_events/accumulator.rs +++ b/crates/analytics/src/auth_events/accumulator.rs @@ -14,6 +14,8 @@ pub struct AuthEventMetricsAccumulator { pub frictionless_flow_count: CountAccumulator, pub frictionless_success_count: CountAccumulator, pub authentication_funnel: CountAccumulator, + pub authentication_exemption_approved_count: CountAccumulator, + pub authentication_exemption_requested_count: CountAccumulator, } #[derive(Debug, Default)] @@ -80,6 +82,12 @@ impl AuthEventMetricsAccumulator { frictionless_success_count: self.frictionless_success_count.collect(), error_message_count: self.authentication_error_message.collect(), authentication_funnel: self.authentication_funnel.collect(), + authentication_exemption_approved_count: self + .authentication_exemption_approved_count + .collect(), + authentication_exemption_requested_count: self + .authentication_exemption_requested_count + .collect(), } } } diff --git a/crates/analytics/src/auth_events/core.rs b/crates/analytics/src/auth_events/core.rs index 4d11f19d53..41b7b72495 100644 --- a/crates/analytics/src/auth_events/core.rs +++ b/crates/analytics/src/auth_events/core.rs @@ -8,15 +8,18 @@ use api_models::analytics::{ AuthEventFilterValue, AuthEventFiltersResponse, AuthEventMetricsResponse, AuthEventsAnalyticsMetadata, GetAuthEventFilterRequest, GetAuthEventMetricRequest, }; +use common_utils::types::TimeRange; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; use super::{ filters::{get_auth_events_filter_for_dimension, AuthEventFilterRow}, + sankey::{get_sankey_data, SankeyRow}, AuthEventMetricsAccumulator, }; use crate::{ auth_events::AuthEventMetricAccumulator, + enums::AuthInfo, errors::{AnalyticsError, AnalyticsResult}, AnalyticsProvider, }; @@ -92,6 +95,12 @@ pub async fn get_metrics( AuthEventMetrics::AuthenticationFunnel => metrics_builder .authentication_funnel .add_metrics_bucket(&value), + AuthEventMetrics::AuthenticationExemptionApprovedCount => metrics_builder + .authentication_exemption_approved_count + .add_metrics_bucket(&value), + AuthEventMetrics::AuthenticationExemptionRequestedCount => metrics_builder + .authentication_exemption_requested_count + .add_metrics_bucket(&value), } } } @@ -170,6 +179,27 @@ pub async fn get_filters( AuthEventDimensions::AuthenticationConnector => fil.authentication_connector.map(|i| i.as_ref().to_string()), AuthEventDimensions::MessageVersion => fil.message_version, AuthEventDimensions::AcsReferenceNumber => fil.acs_reference_number, + AuthEventDimensions::Platform => fil.platform, + AuthEventDimensions::Mcc => fil.mcc, + AuthEventDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + AuthEventDimensions::MerchantCountry => fil.merchant_country, + AuthEventDimensions::BillingCountry => fil.billing_country, + AuthEventDimensions::ShippingCountry => fil.shipping_country, + AuthEventDimensions::IssuerCountry => fil.issuer_country, + AuthEventDimensions::EarliestSupportedVersion => fil.earliest_supported_version, + AuthEventDimensions::LatestSupportedVersion => fil.latest_supported_version, + AuthEventDimensions::WhitelistDecision => fil.whitelist_decision.map(|i| i.to_string()), + AuthEventDimensions::DeviceManufacturer => fil.device_manufacturer, + AuthEventDimensions::DeviceType => fil.device_type, + AuthEventDimensions::DeviceBrand => fil.device_brand, + AuthEventDimensions::DeviceOs => fil.device_os, + AuthEventDimensions::DeviceDisplay => fil.device_display, + AuthEventDimensions::BrowserName => fil.browser_name, + AuthEventDimensions::BrowserVersion => fil.browser_version, + AuthEventDimensions::IssuerId => fil.issuer_id, + AuthEventDimensions::SchemeName => fil.scheme_name, + AuthEventDimensions::ExemptionRequested => fil.exemption_requested.map(|i| i.to_string()), + AuthEventDimensions::ExemptionAccepted => fil.exemption_accepted.map(|i| i.to_string()), }) .collect::>(); res.query_data.push(AuthEventFilterValue { @@ -179,3 +209,24 @@ pub async fn get_filters( } Ok(res) } + +#[instrument(skip_all)] +pub async fn get_sankey( + pool: &AnalyticsProvider, + auth: &AuthInfo, + req: TimeRange, +) -> AnalyticsResult> { + match pool { + AnalyticsProvider::Sqlx(_) => Err(AnalyticsError::NotImplemented( + "Sankey not implemented for sqlx", + ))?, + AnalyticsProvider::Clickhouse(ckh_pool) + | AnalyticsProvider::CombinedCkh(_, ckh_pool) + | AnalyticsProvider::CombinedSqlx(_, ckh_pool) => { + let sankey_rows = get_sankey_data(ckh_pool, auth, &req) + .await + .change_context(AnalyticsError::UnknownError)?; + Ok(sankey_rows) + } + } +} diff --git a/crates/analytics/src/auth_events/filters.rs b/crates/analytics/src/auth_events/filters.rs index 1961116b84..9249613172 100644 --- a/crates/analytics/src/auth_events/filters.rs +++ b/crates/analytics/src/auth_events/filters.rs @@ -1,5 +1,5 @@ use api_models::analytics::{auth_events::AuthEventDimensions, Granularity, TimeRange}; -use common_enums::DecoupledAuthenticationType; +use common_enums::{Currency, DecoupledAuthenticationType}; use common_utils::errors::ReportSwitchExt; use diesel_models::enums::{AuthenticationConnectors, AuthenticationStatus, TransactionStatus}; use error_stack::ResultExt; @@ -60,4 +60,25 @@ pub struct AuthEventFilterRow { pub authentication_connector: Option>, pub message_version: Option, pub acs_reference_number: Option, + pub platform: Option, + pub mcc: Option, + pub currency: Option>, + pub merchant_country: Option, + pub billing_country: Option, + pub shipping_country: Option, + pub issuer_country: Option, + pub earliest_supported_version: Option, + pub latest_supported_version: Option, + pub whitelist_decision: Option, + pub device_manufacturer: Option, + pub device_type: Option, + pub device_brand: Option, + pub device_os: Option, + pub device_display: Option, + pub browser_name: Option, + pub browser_version: Option, + pub issuer_id: Option, + pub scheme_name: Option, + pub exemption_requested: Option, + pub exemption_accepted: Option, } diff --git a/crates/analytics/src/auth_events/metrics.rs b/crates/analytics/src/auth_events/metrics.rs index 6315c1338e..46c3805662 100644 --- a/crates/analytics/src/auth_events/metrics.rs +++ b/crates/analytics/src/auth_events/metrics.rs @@ -17,6 +17,8 @@ use crate::{ mod authentication_attempt_count; mod authentication_count; mod authentication_error_message; +mod authentication_exemption_approved_count; +mod authentication_exemption_requested_count; mod authentication_funnel; mod authentication_success_count; mod challenge_attempt_count; @@ -28,6 +30,8 @@ mod frictionless_success_count; use authentication_attempt_count::AuthenticationAttemptCount; use authentication_count::AuthenticationCount; use authentication_error_message::AuthenticationErrorMessage; +use authentication_exemption_approved_count::AuthenticationExemptionApprovedCount; +use authentication_exemption_requested_count::AuthenticationExemptionRequestedCount; use authentication_funnel::AuthenticationFunnel; use authentication_success_count::AuthenticationSuccessCount; use challenge_attempt_count::ChallengeAttemptCount; @@ -46,6 +50,27 @@ pub struct AuthEventMetricRow { pub authentication_connector: Option>, pub message_version: Option, pub acs_reference_number: Option, + pub platform: Option, + pub mcc: Option, + pub currency: Option>, + pub merchant_country: Option, + pub billing_country: Option, + pub shipping_country: Option, + pub issuer_country: Option, + pub earliest_supported_version: Option, + pub latest_supported_version: Option, + pub whitelist_decision: Option, + pub device_manufacturer: Option, + pub device_type: Option, + pub device_brand: Option, + pub device_os: Option, + pub device_display: Option, + pub browser_name: Option, + pub browser_version: Option, + pub issuer_id: Option, + pub scheme_name: Option, + pub exemption_requested: Option, + pub exemption_accepted: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] @@ -210,6 +235,30 @@ where ) .await } + Self::AuthenticationExemptionApprovedCount => { + AuthenticationExemptionApprovedCount + .load_metrics( + merchant_id, + dimensions, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::AuthenticationExemptionRequestedCount => { + AuthenticationExemptionRequestedCount + .load_metrics( + merchant_id, + dimensions, + filters, + granularity, + time_range, + pool, + ) + .await + } } } } diff --git a/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs b/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs index fbdc4b75b3..967513af32 100644 --- a/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs @@ -111,6 +111,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/authentication_count.rs b/crates/analytics/src/auth_events/metrics/authentication_count.rs index b4c0a5c595..0334f732f2 100644 --- a/crates/analytics/src/auth_events/metrics/authentication_count.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_count.rs @@ -101,6 +101,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/authentication_error_message.rs b/crates/analytics/src/auth_events/metrics/authentication_error_message.rs index b064cb7ab3..ac0adc7bbf 100644 --- a/crates/analytics/src/auth_events/metrics/authentication_error_message.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_error_message.rs @@ -120,6 +120,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/authentication_exemption_approved_count.rs b/crates/analytics/src/auth_events/metrics/authentication_exemption_approved_count.rs new file mode 100644 index 0000000000..89ec3affe6 --- /dev/null +++ b/crates/analytics/src/auth_events/metrics/authentication_exemption_approved_count.rs @@ -0,0 +1,148 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + auth_events::{AuthEventDimensions, AuthEventFilters, 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, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AuthenticationExemptionApprovedCount; + +#[async_trait::async_trait] +impl super::AuthEventMetric for AuthenticationExemptionApprovedCount +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: &common_utils::id_type::MerchantId, + dimensions: &[AuthEventDimensions], + filters: &AuthEventFilters, + granularity: Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::Authentications); + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder + .add_filter_clause(AuthEventDimensions::ExemptionAccepted, true) + .switch()?; + filters.set_filter_clause(&mut query_builder).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 { + granularity + .set_group_by_clause(&mut query_builder) + .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.authentication_status.as_ref().map(|i| i.0), + i.trans_status.as_ref().map(|i| i.0.clone()), + i.authentication_type.as_ref().map(|i| i.0), + i.error_message.clone(), + i.authentication_connector.as_ref().map(|i| i.0), + i.message_version.clone(), + i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/auth_events/metrics/authentication_exemption_requested_count.rs b/crates/analytics/src/auth_events/metrics/authentication_exemption_requested_count.rs new file mode 100644 index 0000000000..aa4af1a47f --- /dev/null +++ b/crates/analytics/src/auth_events/metrics/authentication_exemption_requested_count.rs @@ -0,0 +1,149 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + auth_events::{AuthEventDimensions, AuthEventFilters, 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, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AuthenticationExemptionRequestedCount; + +#[async_trait::async_trait] +impl super::AuthEventMetric for AuthenticationExemptionRequestedCount +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: &common_utils::id_type::MerchantId, + dimensions: &[AuthEventDimensions], + filters: &AuthEventFilters, + granularity: Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::Authentications); + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + query_builder + .add_filter_clause(AuthEventDimensions::ExemptionRequested, true) + .switch()?; + + filters.set_filter_clause(&mut query_builder).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 { + granularity + .set_group_by_clause(&mut query_builder) + .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.authentication_status.as_ref().map(|i| i.0), + i.trans_status.as_ref().map(|i| i.0.clone()), + i.authentication_type.as_ref().map(|i| i.0), + i.error_message.clone(), + i.authentication_connector.as_ref().map(|i| i.0), + i.message_version.clone(), + i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/auth_events/metrics/authentication_funnel.rs b/crates/analytics/src/auth_events/metrics/authentication_funnel.rs index 3a122fd42a..145a70a542 100644 --- a/crates/analytics/src/auth_events/metrics/authentication_funnel.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_funnel.rs @@ -112,6 +112,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/authentication_success_count.rs b/crates/analytics/src/auth_events/metrics/authentication_success_count.rs index 3743512362..9788b6477c 100644 --- a/crates/analytics/src/auth_events/metrics/authentication_success_count.rs +++ b/crates/analytics/src/auth_events/metrics/authentication_success_count.rs @@ -106,6 +106,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs b/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs index 0265fc7e45..d3d7dcc747 100644 --- a/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs +++ b/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs @@ -116,6 +116,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs b/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs index 178dcba20b..5b951e1fa8 100644 --- a/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs +++ b/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs @@ -108,6 +108,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/challenge_success_count.rs b/crates/analytics/src/auth_events/metrics/challenge_success_count.rs index b78f84326d..2bbd4d8198 100644 --- a/crates/analytics/src/auth_events/metrics/challenge_success_count.rs +++ b/crates/analytics/src/auth_events/metrics/challenge_success_count.rs @@ -113,6 +113,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs b/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs index d114e36f14..da8a119d22 100644 --- a/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs +++ b/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs @@ -109,6 +109,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs b/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs index a56d5b660a..b4fb18e48f 100644 --- a/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs +++ b/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs @@ -113,6 +113,26 @@ where i.authentication_connector.as_ref().map(|i| i.0), i.message_version.clone(), i.acs_reference_number.clone(), + i.mcc.clone(), + i.currency.as_ref().map(|i| i.0), + i.merchant_country.clone(), + i.billing_country.clone(), + i.shipping_country.clone(), + i.issuer_country.clone(), + i.earliest_supported_version.clone(), + i.latest_supported_version.clone(), + i.whitelist_decision, + i.device_manufacturer.clone(), + i.device_type.clone(), + i.device_brand.clone(), + i.device_os.clone(), + i.device_display.clone(), + i.browser_name.clone(), + i.browser_version.clone(), + i.issuer_id.clone(), + i.scheme_name.clone(), + i.exemption_requested, + i.exemption_accepted, TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/auth_events/sankey.rs b/crates/analytics/src/auth_events/sankey.rs new file mode 100644 index 0000000000..ede90ad9a8 --- /dev/null +++ b/crates/analytics/src/auth_events/sankey.rs @@ -0,0 +1,88 @@ +use common_enums::AuthenticationStatus; +use common_utils::{ + errors::ParsingError, + types::{authentication::AuthInfo, TimeRange}, +}; +use error_stack::ResultExt; +use router_env::logger; + +use crate::{ + clickhouse::ClickhouseClient, + query::{Aggregate, QueryBuilder, QueryFilter}, + types::{AnalyticsCollection, MetricsError, MetricsResult}, +}; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SankeyRow { + pub count: i64, + pub authentication_status: Option, + pub exemption_requested: Option, + pub exemption_accepted: Option, +} + +impl TryInto for serde_json::Value { + type Error = error_stack::Report; + + fn try_into(self) -> Result { + logger::debug!("Parsing SankeyRow from {:?}", self); + serde_json::from_value(self).change_context(ParsingError::StructParseFailure( + "Failed to parse Sankey in clickhouse results", + )) + } +} + +pub async fn get_sankey_data( + clickhouse_client: &ClickhouseClient, + auth: &AuthInfo, + time_range: &TimeRange, +) -> MetricsResult> { + let mut query_builder = + QueryBuilder::::new(AnalyticsCollection::Authentications); + + query_builder + .add_select_column(Aggregate::::Count { + field: None, + alias: Some("count"), + }) + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_select_column("exemption_requested") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_select_column("exemption_accepted") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_select_column("authentication_status") + .change_context(MetricsError::QueryBuildingError)?; + + auth.set_filter_clause(&mut query_builder) + .change_context(MetricsError::QueryBuildingError)?; + + time_range + .set_filter_clause(&mut query_builder) + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_group_by_clause("exemption_requested") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_group_by_clause("exemption_accepted") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .add_group_by_clause("authentication_status") + .change_context(MetricsError::QueryBuildingError)?; + + query_builder + .execute_query::(clickhouse_client) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(Ok) + .collect() +} diff --git a/crates/analytics/src/auth_events/types.rs b/crates/analytics/src/auth_events/types.rs index 863e50a0af..4aaa262267 100644 --- a/crates/analytics/src/auth_events/types.rs +++ b/crates/analytics/src/auth_events/types.rs @@ -54,6 +54,12 @@ where .attach_printable("Error adding message version filter")?; } + if !self.platform.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::Platform, &self.platform) + .attach_printable("Error adding platform filter")?; + } + if !self.acs_reference_number.is_empty() { builder .add_filter_in_range_clause( @@ -62,6 +68,144 @@ where ) .attach_printable("Error adding acs reference number filter")?; } + + if !self.mcc.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::Mcc, &self.mcc) + .attach_printable("Failed to add MCC filter")?; + } + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::Currency, &self.currency) + .attach_printable("Failed to add currency filter")?; + } + if !self.merchant_country.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::MerchantCountry, + &self.merchant_country, + ) + .attach_printable("Failed to add merchant country filter")?; + } + if !self.billing_country.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::BillingCountry, + &self.billing_country, + ) + .attach_printable("Failed to add billing country filter")?; + } + if !self.shipping_country.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::ShippingCountry, + &self.shipping_country, + ) + .attach_printable("Failed to add shipping country filter")?; + } + if !self.issuer_country.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::IssuerCountry, + &self.issuer_country, + ) + .attach_printable("Failed to add issuer country filter")?; + } + if !self.earliest_supported_version.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::EarliestSupportedVersion, + &self.earliest_supported_version, + ) + .attach_printable("Failed to add earliest supported version filter")?; + } + if !self.latest_supported_version.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::LatestSupportedVersion, + &self.latest_supported_version, + ) + .attach_printable("Failed to add latest supported version filter")?; + } + if !self.whitelist_decision.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::WhitelistDecision, + &self.whitelist_decision, + ) + .attach_printable("Failed to add whitelist decision filter")?; + } + if !self.device_manufacturer.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::DeviceManufacturer, + &self.device_manufacturer, + ) + .attach_printable("Failed to add device manufacturer filter")?; + } + if !self.device_type.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::DeviceType, &self.device_type) + .attach_printable("Failed to add device type filter")?; + } + if !self.device_brand.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::DeviceBrand, &self.device_brand) + .attach_printable("Failed to add device brand filter")?; + } + if !self.device_os.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::DeviceOs, &self.device_os) + .attach_printable("Failed to add device OS filter")?; + } + if !self.device_display.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::DeviceDisplay, + &self.device_display, + ) + .attach_printable("Failed to add device display filter")?; + } + if !self.browser_name.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::BrowserName, &self.browser_name) + .attach_printable("Failed to add browser name filter")?; + } + if !self.browser_version.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::BrowserVersion, + &self.browser_version, + ) + .attach_printable("Failed to add browser version filter")?; + } + if !self.issuer_id.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::IssuerId, &self.issuer_id) + .attach_printable("Failed to add issuer ID filter")?; + } + if !self.scheme_name.is_empty() { + builder + .add_filter_in_range_clause(AuthEventDimensions::SchemeName, &self.scheme_name) + .attach_printable("Failed to add scheme name filter")?; + } + if !self.exemption_requested.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::ExemptionRequested, + &self.exemption_requested, + ) + .attach_printable("Failed to add exemption requested filter")?; + } + if !self.exemption_accepted.is_empty() { + builder + .add_filter_in_range_clause( + AuthEventDimensions::ExemptionAccepted, + &self.exemption_accepted, + ) + .attach_printable("Failed to add exemption accepted filter")?; + } + Ok(()) } } diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 5a2f097bb0..7934c91184 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -231,6 +231,11 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::metrics::AuthEventMetricRow ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + + let platform: Option = row.try_get("platform").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let acs_reference_number: Option = row.try_get("acs_reference_number").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), @@ -247,6 +252,102 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::metrics::AuthEventMetricRow let end_bucket: Option = row .try_get::, _>("end_bucket")? .and_then(|dt| dt.replace_millisecond(0).ok()); + let mcc: Option = row.try_get("mcc").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let merchant_country: Option = + row.try_get("merchant_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let billing_country: Option = + row.try_get("billing_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let shipping_country: Option = + row.try_get("shipping_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let issuer_country: Option = + row.try_get("issuer_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let earliest_supported_version: Option = row + .try_get("earliest_supported_version") + .or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let latest_supported_version: Option = row + .try_get("latest_supported_version") + .or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let whitelist_decision: Option = + row.try_get("whitelist_decision").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_manufacturer: Option = + row.try_get("device_manufacturer").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_type: Option = row.try_get("device_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_brand: Option = row.try_get("device_brand").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_os: Option = row.try_get("device_os").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_display: Option = + row.try_get("device_display").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let browser_name: Option = row.try_get("browser_name").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let browser_version: Option = + row.try_get("browser_version").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let issuer_id: Option = row.try_get("issuer_id").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let scheme_name: Option = row.try_get("scheme_name").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let exemption_requested: Option = + row.try_get("exemption_requested").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let exemption_accepted: Option = + row.try_get("exemption_accepted").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { authentication_status, trans_status, @@ -255,9 +356,30 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::metrics::AuthEventMetricRow authentication_connector, message_version, acs_reference_number, + platform, count, start_bucket, end_bucket, + mcc, + currency, + merchant_country, + billing_country, + shipping_country, + issuer_country, + earliest_supported_version, + latest_supported_version, + whitelist_decision, + device_manufacturer, + device_type, + device_brand, + device_os, + device_display, + browser_name, + browser_version, + issuer_id, + scheme_name, + exemption_requested, + exemption_accepted, }) } } @@ -299,6 +421,106 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::filters::AuthEventFilterRow ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let platform: Option = row.try_get("platform").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let mcc: Option = row.try_get("mcc").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let merchant_country: Option = + row.try_get("merchant_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let billing_country: Option = + row.try_get("billing_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let shipping_country: Option = + row.try_get("shipping_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let issuer_country: Option = + row.try_get("issuer_country").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let earliest_supported_version: Option = row + .try_get("earliest_supported_version") + .or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let latest_supported_version: Option = row + .try_get("latest_supported_version") + .or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let whitelist_decision: Option = + row.try_get("whitelist_decision").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_manufacturer: Option = + row.try_get("device_manufacturer").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_type: Option = row.try_get("device_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_brand: Option = row.try_get("device_brand").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_os: Option = row.try_get("device_os").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let device_display: Option = + row.try_get("device_display").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let browser_name: Option = row.try_get("browser_name").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let browser_version: Option = + row.try_get("browser_version").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let issuer_id: Option = row.try_get("issuer_id").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let scheme_name: Option = row.try_get("scheme_name").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let exemption_requested: Option = + row.try_get("exemption_requested").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let exemption_accepted: Option = + row.try_get("exemption_accepted").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { authentication_status, trans_status, @@ -306,7 +528,28 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::filters::AuthEventFilterRow error_message, authentication_connector, message_version, + platform, acs_reference_number, + mcc, + currency, + merchant_country, + billing_country, + shipping_country, + issuer_country, + earliest_supported_version, + latest_supported_version, + whitelist_decision, + device_manufacturer, + device_type, + device_brand, + device_os, + device_display, + browser_name, + browser_version, + issuer_id, + scheme_name, + exemption_requested, + exemption_accepted, }) } } diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs index 0832788e26..06cc998f3d 100644 --- a/crates/analytics/src/utils.rs +++ b/crates/analytics/src/utils.rs @@ -52,6 +52,27 @@ pub fn get_auth_event_dimensions() -> Vec { AuthEventDimensions::AuthenticationConnector, AuthEventDimensions::MessageVersion, AuthEventDimensions::AcsReferenceNumber, + AuthEventDimensions::Platform, + AuthEventDimensions::Mcc, + AuthEventDimensions::Currency, + AuthEventDimensions::MerchantCountry, + AuthEventDimensions::BillingCountry, + AuthEventDimensions::ShippingCountry, + AuthEventDimensions::IssuerCountry, + AuthEventDimensions::IssuerId, + AuthEventDimensions::EarliestSupportedVersion, + AuthEventDimensions::LatestSupportedVersion, + AuthEventDimensions::WhitelistDecision, + AuthEventDimensions::DeviceManufacturer, + AuthEventDimensions::DeviceType, + AuthEventDimensions::DeviceBrand, + AuthEventDimensions::DeviceOs, + AuthEventDimensions::DeviceDisplay, + AuthEventDimensions::BrowserName, + AuthEventDimensions::BrowserVersion, + AuthEventDimensions::SchemeName, + AuthEventDimensions::ExemptionRequested, + AuthEventDimensions::ExemptionAccepted, ] .into_iter() .map(Into::into) diff --git a/crates/api_models/src/analytics/auth_events.rs b/crates/api_models/src/analytics/auth_events.rs index 8a1c168aa2..f1062e0269 100644 --- a/crates/api_models/src/analytics/auth_events.rs +++ b/crates/api_models/src/analytics/auth_events.rs @@ -4,7 +4,8 @@ use std::{ }; use common_enums::{ - AuthenticationConnectors, AuthenticationStatus, DecoupledAuthenticationType, TransactionStatus, + AuthenticationConnectors, AuthenticationStatus, Currency, DecoupledAuthenticationType, + TransactionStatus, }; use super::{NameDescription, TimeRange}; @@ -24,7 +25,49 @@ pub struct AuthEventFilters { #[serde(default)] pub message_version: Vec, #[serde(default)] + pub platform: Vec, + #[serde(default)] pub acs_reference_number: Vec, + #[serde(default)] + pub mcc: Vec, + #[serde(default)] + pub currency: Vec, + #[serde(default)] + pub merchant_country: Vec, + #[serde(default)] + pub billing_country: Vec, + #[serde(default)] + pub shipping_country: Vec, + #[serde(default)] + pub issuer_country: Vec, + #[serde(default)] + pub earliest_supported_version: Vec, + #[serde(default)] + pub latest_supported_version: Vec, + #[serde(default)] + pub whitelist_decision: Vec, + #[serde(default)] + pub device_manufacturer: Vec, + #[serde(default)] + pub device_type: Vec, + #[serde(default)] + pub device_brand: Vec, + #[serde(default)] + pub device_os: Vec, + #[serde(default)] + pub device_display: Vec, + #[serde(default)] + pub browser_name: Vec, + #[serde(default)] + pub browser_version: Vec, + #[serde(default)] + pub issuer_id: Vec, + #[serde(default)] + pub scheme_name: Vec, + #[serde(default)] + pub exemption_requested: Vec, + #[serde(default)] + pub exemption_accepted: Vec, } #[derive( @@ -53,6 +96,27 @@ pub enum AuthEventDimensions { AuthenticationConnector, MessageVersion, AcsReferenceNumber, + Platform, + Mcc, + Currency, + MerchantCountry, + BillingCountry, + ShippingCountry, + IssuerCountry, + EarliestSupportedVersion, + LatestSupportedVersion, + WhitelistDecision, + DeviceManufacturer, + DeviceType, + DeviceBrand, + DeviceOs, + DeviceDisplay, + BrowserName, + BrowserVersion, + IssuerId, + SchemeName, + ExemptionRequested, + ExemptionAccepted, } #[derive( @@ -80,6 +144,8 @@ pub enum AuthEventMetrics { ChallengeSuccessCount, AuthenticationErrorMessage, AuthenticationFunnel, + AuthenticationExemptionApprovedCount, + AuthenticationExemptionRequestedCount, } #[derive( @@ -139,6 +205,26 @@ pub struct AuthEventMetricsBucketIdentifier { pub authentication_connector: Option, pub message_version: Option, pub acs_reference_number: Option, + pub mcc: Option, + pub currency: Option, + pub merchant_country: Option, + pub billing_country: Option, + pub shipping_country: Option, + pub issuer_country: Option, + pub earliest_supported_version: Option, + pub latest_supported_version: Option, + pub whitelist_decision: Option, + pub device_manufacturer: Option, + pub device_type: Option, + pub device_brand: Option, + pub device_os: Option, + pub device_display: Option, + pub browser_name: Option, + pub browser_version: Option, + pub issuer_id: Option, + pub scheme_name: Option, + pub exemption_requested: Option, + pub exemption_accepted: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, #[serde(rename = "time_bucket")] @@ -156,6 +242,26 @@ impl AuthEventMetricsBucketIdentifier { authentication_connector: Option, message_version: Option, acs_reference_number: Option, + mcc: Option, + currency: Option, + merchant_country: Option, + billing_country: Option, + shipping_country: Option, + issuer_country: Option, + earliest_supported_version: Option, + latest_supported_version: Option, + whitelist_decision: Option, + device_manufacturer: Option, + device_type: Option, + device_brand: Option, + device_os: Option, + device_display: Option, + browser_name: Option, + browser_version: Option, + issuer_id: Option, + scheme_name: Option, + exemption_requested: Option, + exemption_accepted: Option, normalized_time_range: TimeRange, ) -> Self { Self { @@ -166,6 +272,26 @@ impl AuthEventMetricsBucketIdentifier { authentication_connector, message_version, acs_reference_number, + mcc, + currency, + merchant_country, + billing_country, + shipping_country, + issuer_country, + earliest_supported_version, + latest_supported_version, + whitelist_decision, + device_manufacturer, + device_type, + device_brand, + device_os, + device_display, + browser_name, + browser_version, + issuer_id, + scheme_name, + exemption_requested, + exemption_accepted, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } @@ -181,6 +307,26 @@ impl Hash for AuthEventMetricsBucketIdentifier { self.message_version.hash(state); self.acs_reference_number.hash(state); self.error_message.hash(state); + self.mcc.hash(state); + self.currency.hash(state); + self.merchant_country.hash(state); + self.billing_country.hash(state); + self.shipping_country.hash(state); + self.issuer_country.hash(state); + self.earliest_supported_version.hash(state); + self.latest_supported_version.hash(state); + self.whitelist_decision.hash(state); + self.device_manufacturer.hash(state); + self.device_type.hash(state); + self.device_brand.hash(state); + self.device_os.hash(state); + self.device_display.hash(state); + self.browser_name.hash(state); + self.browser_version.hash(state); + self.issuer_id.hash(state); + self.scheme_name.hash(state); + self.exemption_requested.hash(state); + self.exemption_accepted.hash(state); self.time_bucket.hash(state); } } @@ -207,6 +353,8 @@ pub struct AuthEventMetricsBucketValue { pub frictionless_success_count: Option, pub error_message_count: Option, pub authentication_funnel: Option, + pub authentication_exemption_approved_count: Option, + pub authentication_exemption_requested_count: Option, } #[derive(Debug, serde::Serialize)] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 7aaebfc07c..67722d3e25 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -110,6 +110,10 @@ pub mod routes { web::resource("metrics/auth_events") .route(web::post().to(get_auth_event_metrics)), ) + .service( + web::resource("metrics/auth_events/sankey") + .route(web::post().to(get_auth_event_sankey)), + ) .service( web::resource("filters/auth_events") .route(web::post().to(get_merchant_auth_events_filters)), @@ -2727,6 +2731,37 @@ pub mod routes { .await } + pub async fn get_auth_event_sankey( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + ) -> impl Responder { + let flow = AnalyticsFlow::GetSankey; + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req, _| async move { + let org_id = auth.merchant_account.get_org_id(); + let merchant_id = auth.merchant_account.get_id(); + let auth: AuthInfo = AuthInfo::MerchantLevel { + org_id: org_id.clone(), + merchant_ids: vec![merchant_id.clone()], + }; + analytics::auth_events::get_sankey(&state.pool, &auth, req) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth { + permission: Permission::MerchantAnalyticsRead, + }, + api_locking::LockAction::NotApplicable, + )) + .await + } + #[cfg(feature = "v1")] pub async fn get_org_sankey( state: web::Data,