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:
Sanskar Atrey
2025-06-02 15:08:00 +05:30
committed by GitHub
parent c26d04a9d3
commit 55f6dbe319
23 changed files with 1309 additions and 3 deletions

View File

@ -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};

View File

@ -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(),
}
}
}

View File

@ -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)
}
}
}

View File

@ -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>,
}

View File

@ -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
}
}
}
}

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)?,

View File

@ -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)?,

View 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()
}

View File

@ -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(())
}
}

View File

@ -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,
})
}
}

View File

@ -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)

View File

@ -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)]

View File

@ -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>,