mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 12:15:40 +08:00
feat(analytics): revamped 3ds auth analytics (#8163)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -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};
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::<Vec<String>>();
|
||||
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<Vec<SankeyRow>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<DBEnumWrapper<AuthenticationConnectors>>,
|
||||
pub message_version: Option<String>,
|
||||
pub acs_reference_number: Option<String>,
|
||||
pub platform: Option<String>,
|
||||
pub mcc: Option<String>,
|
||||
pub currency: Option<DBEnumWrapper<Currency>>,
|
||||
pub merchant_country: Option<String>,
|
||||
pub billing_country: Option<String>,
|
||||
pub shipping_country: Option<String>,
|
||||
pub issuer_country: Option<String>,
|
||||
pub earliest_supported_version: Option<String>,
|
||||
pub latest_supported_version: Option<String>,
|
||||
pub whitelist_decision: Option<bool>,
|
||||
pub device_manufacturer: Option<String>,
|
||||
pub device_type: Option<String>,
|
||||
pub device_brand: Option<String>,
|
||||
pub device_os: Option<String>,
|
||||
pub device_display: Option<String>,
|
||||
pub browser_name: Option<String>,
|
||||
pub browser_version: Option<String>,
|
||||
pub issuer_id: Option<String>,
|
||||
pub scheme_name: Option<String>,
|
||||
pub exemption_requested: Option<bool>,
|
||||
pub exemption_accepted: Option<bool>,
|
||||
}
|
||||
|
||||
@ -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<DBEnumWrapper<storage_enums::AuthenticationConnectors>>,
|
||||
pub message_version: Option<String>,
|
||||
pub acs_reference_number: Option<String>,
|
||||
pub platform: Option<String>,
|
||||
pub mcc: Option<String>,
|
||||
pub currency: Option<DBEnumWrapper<storage_enums::Currency>>,
|
||||
pub merchant_country: Option<String>,
|
||||
pub billing_country: Option<String>,
|
||||
pub shipping_country: Option<String>,
|
||||
pub issuer_country: Option<String>,
|
||||
pub earliest_supported_version: Option<String>,
|
||||
pub latest_supported_version: Option<String>,
|
||||
pub whitelist_decision: Option<bool>,
|
||||
pub device_manufacturer: Option<String>,
|
||||
pub device_type: Option<String>,
|
||||
pub device_brand: Option<String>,
|
||||
pub device_os: Option<String>,
|
||||
pub device_display: Option<String>,
|
||||
pub browser_name: Option<String>,
|
||||
pub browser_version: Option<String>,
|
||||
pub issuer_id: Option<String>,
|
||||
pub scheme_name: Option<String>,
|
||||
pub exemption_requested: Option<bool>,
|
||||
pub exemption_accepted: Option<bool>,
|
||||
#[serde(with = "common_utils::custom_serde::iso8601::option")]
|
||||
pub start_bucket: Option<PrimitiveDateTime>,
|
||||
#[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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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<T> super::AuthEventMetric<T> for AuthenticationExemptionApprovedCount
|
||||
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: &common_utils::id_type::MerchantId,
|
||||
dimensions: &[AuthEventDimensions],
|
||||
filters: &AuthEventFilters,
|
||||
granularity: Option<Granularity>,
|
||||
time_range: &TimeRange,
|
||||
pool: &T,
|
||||
) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
|
||||
let mut query_builder: QueryBuilder<T> =
|
||||
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::<AuthEventMetricRow, _>(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::<error_stack::Result<
|
||||
HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
|
||||
crate::query::PostProcessingError,
|
||||
>>()
|
||||
.change_context(MetricsError::PostProcessingFailure)
|
||||
}
|
||||
}
|
||||
@ -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<T> super::AuthEventMetric<T> for AuthenticationExemptionRequestedCount
|
||||
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: &common_utils::id_type::MerchantId,
|
||||
dimensions: &[AuthEventDimensions],
|
||||
filters: &AuthEventFilters,
|
||||
granularity: Option<Granularity>,
|
||||
time_range: &TimeRange,
|
||||
pool: &T,
|
||||
) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
|
||||
let mut query_builder: QueryBuilder<T> =
|
||||
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::<AuthEventMetricRow, _>(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::<error_stack::Result<
|
||||
HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
|
||||
crate::query::PostProcessingError,
|
||||
>>()
|
||||
.change_context(MetricsError::PostProcessingFailure)
|
||||
}
|
||||
}
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
@ -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)?,
|
||||
|
||||
88
crates/analytics/src/auth_events/sankey.rs
Normal file
88
crates/analytics/src/auth_events/sankey.rs
Normal file
@ -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<AuthenticationStatus>,
|
||||
pub exemption_requested: Option<bool>,
|
||||
pub exemption_accepted: Option<bool>,
|
||||
}
|
||||
|
||||
impl TryInto<SankeyRow> for serde_json::Value {
|
||||
type Error = error_stack::Report<ParsingError>;
|
||||
|
||||
fn try_into(self) -> Result<SankeyRow, Self::Error> {
|
||||
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<Vec<SankeyRow>> {
|
||||
let mut query_builder =
|
||||
QueryBuilder::<ClickhouseClient>::new(AnalyticsCollection::Authentications);
|
||||
|
||||
query_builder
|
||||
.add_select_column(Aggregate::<String>::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::<SankeyRow, _>(clickhouse_client)
|
||||
.await
|
||||
.change_context(MetricsError::QueryBuildingError)?
|
||||
.change_context(MetricsError::QueryExecutionFailure)?
|
||||
.into_iter()
|
||||
.map(Ok)
|
||||
.collect()
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String> = row.try_get("platform").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let acs_reference_number: Option<String> =
|
||||
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<PrimitiveDateTime> = row
|
||||
.try_get::<Option<PrimitiveDateTime>, _>("end_bucket")?
|
||||
.and_then(|dt| dt.replace_millisecond(0).ok());
|
||||
let mcc: Option<String> = row.try_get("mcc").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let currency: Option<DBEnumWrapper<Currency>> =
|
||||
row.try_get("currency").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let merchant_country: Option<String> =
|
||||
row.try_get("merchant_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let billing_country: Option<String> =
|
||||
row.try_get("billing_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let shipping_country: Option<String> =
|
||||
row.try_get("shipping_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let issuer_country: Option<String> =
|
||||
row.try_get("issuer_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let earliest_supported_version: Option<String> = row
|
||||
.try_get("earliest_supported_version")
|
||||
.or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let latest_supported_version: Option<String> = row
|
||||
.try_get("latest_supported_version")
|
||||
.or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let whitelist_decision: Option<bool> =
|
||||
row.try_get("whitelist_decision").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_manufacturer: Option<String> =
|
||||
row.try_get("device_manufacturer").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_type: Option<String> = row.try_get("device_type").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_brand: Option<String> = row.try_get("device_brand").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_os: Option<String> = row.try_get("device_os").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_display: Option<String> =
|
||||
row.try_get("device_display").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let browser_name: Option<String> = row.try_get("browser_name").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let browser_version: Option<String> =
|
||||
row.try_get("browser_version").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let issuer_id: Option<String> = row.try_get("issuer_id").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let scheme_name: Option<String> = row.try_get("scheme_name").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let exemption_requested: Option<bool> =
|
||||
row.try_get("exemption_requested").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let exemption_accepted: Option<bool> =
|
||||
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<String> = row.try_get("platform").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let mcc: Option<String> = row.try_get("mcc").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let currency: Option<DBEnumWrapper<Currency>> =
|
||||
row.try_get("currency").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let merchant_country: Option<String> =
|
||||
row.try_get("merchant_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let billing_country: Option<String> =
|
||||
row.try_get("billing_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let shipping_country: Option<String> =
|
||||
row.try_get("shipping_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let issuer_country: Option<String> =
|
||||
row.try_get("issuer_country").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let earliest_supported_version: Option<String> = row
|
||||
.try_get("earliest_supported_version")
|
||||
.or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let latest_supported_version: Option<String> = row
|
||||
.try_get("latest_supported_version")
|
||||
.or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let whitelist_decision: Option<bool> =
|
||||
row.try_get("whitelist_decision").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_manufacturer: Option<String> =
|
||||
row.try_get("device_manufacturer").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_type: Option<String> = row.try_get("device_type").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_brand: Option<String> = row.try_get("device_brand").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_os: Option<String> = row.try_get("device_os").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let device_display: Option<String> =
|
||||
row.try_get("device_display").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let browser_name: Option<String> = row.try_get("browser_name").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let browser_version: Option<String> =
|
||||
row.try_get("browser_version").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let issuer_id: Option<String> = row.try_get("issuer_id").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let scheme_name: Option<String> = row.try_get("scheme_name").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let exemption_requested: Option<bool> =
|
||||
row.try_get("exemption_requested").or_else(|e| match e {
|
||||
ColumnNotFound(_) => Ok(Default::default()),
|
||||
e => Err(e),
|
||||
})?;
|
||||
let exemption_accepted: Option<bool> =
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,27 @@ pub fn get_auth_event_dimensions() -> Vec<NameDescription> {
|
||||
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)
|
||||
|
||||
@ -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<String>,
|
||||
#[serde(default)]
|
||||
pub platform: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub acs_reference_number: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mcc: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub currency: Vec<Currency>,
|
||||
#[serde(default)]
|
||||
pub merchant_country: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub billing_country: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub shipping_country: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub issuer_country: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub earliest_supported_version: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub latest_supported_version: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub whitelist_decision: Vec<bool>,
|
||||
#[serde(default)]
|
||||
pub device_manufacturer: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub device_type: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub device_brand: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub device_os: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub device_display: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub browser_name: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub browser_version: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub issuer_id: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub scheme_name: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub exemption_requested: Vec<bool>,
|
||||
#[serde(default)]
|
||||
pub exemption_accepted: Vec<bool>,
|
||||
}
|
||||
|
||||
#[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<AuthenticationConnectors>,
|
||||
pub message_version: Option<String>,
|
||||
pub acs_reference_number: Option<String>,
|
||||
pub mcc: Option<String>,
|
||||
pub currency: Option<Currency>,
|
||||
pub merchant_country: Option<String>,
|
||||
pub billing_country: Option<String>,
|
||||
pub shipping_country: Option<String>,
|
||||
pub issuer_country: Option<String>,
|
||||
pub earliest_supported_version: Option<String>,
|
||||
pub latest_supported_version: Option<String>,
|
||||
pub whitelist_decision: Option<bool>,
|
||||
pub device_manufacturer: Option<String>,
|
||||
pub device_type: Option<String>,
|
||||
pub device_brand: Option<String>,
|
||||
pub device_os: Option<String>,
|
||||
pub device_display: Option<String>,
|
||||
pub browser_name: Option<String>,
|
||||
pub browser_version: Option<String>,
|
||||
pub issuer_id: Option<String>,
|
||||
pub scheme_name: Option<String>,
|
||||
pub exemption_requested: Option<bool>,
|
||||
pub exemption_accepted: Option<bool>,
|
||||
#[serde(rename = "time_range")]
|
||||
pub time_bucket: TimeRange,
|
||||
#[serde(rename = "time_bucket")]
|
||||
@ -156,6 +242,26 @@ impl AuthEventMetricsBucketIdentifier {
|
||||
authentication_connector: Option<AuthenticationConnectors>,
|
||||
message_version: Option<String>,
|
||||
acs_reference_number: Option<String>,
|
||||
mcc: Option<String>,
|
||||
currency: Option<Currency>,
|
||||
merchant_country: Option<String>,
|
||||
billing_country: Option<String>,
|
||||
shipping_country: Option<String>,
|
||||
issuer_country: Option<String>,
|
||||
earliest_supported_version: Option<String>,
|
||||
latest_supported_version: Option<String>,
|
||||
whitelist_decision: Option<bool>,
|
||||
device_manufacturer: Option<String>,
|
||||
device_type: Option<String>,
|
||||
device_brand: Option<String>,
|
||||
device_os: Option<String>,
|
||||
device_display: Option<String>,
|
||||
browser_name: Option<String>,
|
||||
browser_version: Option<String>,
|
||||
issuer_id: Option<String>,
|
||||
scheme_name: Option<String>,
|
||||
exemption_requested: Option<bool>,
|
||||
exemption_accepted: Option<bool>,
|
||||
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<u64>,
|
||||
pub error_message_count: Option<u64>,
|
||||
pub authentication_funnel: Option<u64>,
|
||||
pub authentication_exemption_approved_count: Option<u64>,
|
||||
pub authentication_exemption_requested_count: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
|
||||
@ -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<AppState>,
|
||||
req: actix_web::HttpRequest,
|
||||
json_payload: web::Json<TimeRange>,
|
||||
) -> 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<AppState>,
|
||||
|
||||
Reference in New Issue
Block a user