Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(analytics): authentication analytics (#4429)
Co-authored-by: Sampras Lopes <lsampras@pm.me> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
- Loading branch information
1 parent
86e0550
commit 24d1542
Showing
28 changed files
with
790 additions
and
418 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
pub mod accumulator; | ||
mod core; | ||
pub mod metrics; | ||
pub use accumulator::{AuthEventMetricAccumulator, AuthEventMetricsAccumulator}; | ||
|
||
pub use self::core::get_metrics; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
use api_models::analytics::auth_events::AuthEventMetricsBucketValue; | ||
|
||
use super::metrics::AuthEventMetricRow; | ||
|
||
#[derive(Debug, Default)] | ||
pub struct AuthEventMetricsAccumulator { | ||
pub three_ds_sdk_count: CountAccumulator, | ||
pub authentication_attempt_count: CountAccumulator, | ||
pub authentication_success_count: CountAccumulator, | ||
pub challenge_flow_count: CountAccumulator, | ||
pub challenge_attempt_count: CountAccumulator, | ||
pub challenge_success_count: CountAccumulator, | ||
pub frictionless_flow_count: CountAccumulator, | ||
} | ||
|
||
#[derive(Debug, Default)] | ||
#[repr(transparent)] | ||
pub struct CountAccumulator { | ||
pub count: Option<i64>, | ||
} | ||
|
||
pub trait AuthEventMetricAccumulator { | ||
type MetricOutput; | ||
|
||
fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow); | ||
|
||
fn collect(self) -> Self::MetricOutput; | ||
} | ||
|
||
impl AuthEventMetricAccumulator for CountAccumulator { | ||
type MetricOutput = Option<u64>; | ||
#[inline] | ||
fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow) { | ||
self.count = match (self.count, metrics.count) { | ||
(None, None) => None, | ||
(None, i @ Some(_)) | (i @ Some(_), None) => i, | ||
(Some(a), Some(b)) => Some(a + b), | ||
} | ||
} | ||
#[inline] | ||
fn collect(self) -> Self::MetricOutput { | ||
self.count.and_then(|i| u64::try_from(i).ok()) | ||
} | ||
} | ||
|
||
impl AuthEventMetricsAccumulator { | ||
pub fn collect(self) -> AuthEventMetricsBucketValue { | ||
AuthEventMetricsBucketValue { | ||
three_ds_sdk_count: self.three_ds_sdk_count.collect(), | ||
authentication_attempt_count: self.authentication_attempt_count.collect(), | ||
authentication_success_count: self.authentication_success_count.collect(), | ||
challenge_flow_count: self.challenge_flow_count.collect(), | ||
challenge_attempt_count: self.challenge_attempt_count.collect(), | ||
challenge_success_count: self.challenge_success_count.collect(), | ||
frictionless_flow_count: self.frictionless_flow_count.collect(), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
use std::collections::HashMap; | ||
|
||
use api_models::analytics::{ | ||
auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier, MetricsBucketResponse}, | ||
AnalyticsMetadata, GetAuthEventMetricRequest, MetricsResponse, | ||
}; | ||
use error_stack::ResultExt; | ||
use router_env::{instrument, logger, tracing}; | ||
|
||
use super::AuthEventMetricsAccumulator; | ||
use crate::{ | ||
auth_events::AuthEventMetricAccumulator, | ||
errors::{AnalyticsError, AnalyticsResult}, | ||
AnalyticsProvider, | ||
}; | ||
|
||
#[instrument(skip_all)] | ||
pub async fn get_metrics( | ||
pool: &AnalyticsProvider, | ||
merchant_id: &String, | ||
publishable_key: Option<&String>, | ||
req: GetAuthEventMetricRequest, | ||
) -> AnalyticsResult<MetricsResponse<MetricsBucketResponse>> { | ||
let mut metrics_accumulator: HashMap< | ||
AuthEventMetricsBucketIdentifier, | ||
AuthEventMetricsAccumulator, | ||
> = HashMap::new(); | ||
|
||
if let Some(publishable_key) = publishable_key { | ||
let mut set = tokio::task::JoinSet::new(); | ||
for metric_type in req.metrics.iter().cloned() { | ||
let req = req.clone(); | ||
let merchant_id_scoped = merchant_id.to_owned(); | ||
let publishable_key_scoped = publishable_key.to_owned(); | ||
let pool = pool.clone(); | ||
set.spawn(async move { | ||
let data = pool | ||
.get_auth_event_metrics( | ||
&metric_type, | ||
&merchant_id_scoped, | ||
&publishable_key_scoped, | ||
&req.time_series.map(|t| t.granularity), | ||
&req.time_range, | ||
) | ||
.await | ||
.change_context(AnalyticsError::UnknownError); | ||
(metric_type, data) | ||
}); | ||
} | ||
|
||
while let Some((metric, data)) = set | ||
.join_next() | ||
.await | ||
.transpose() | ||
.change_context(AnalyticsError::UnknownError)? | ||
{ | ||
for (id, value) in data? { | ||
let metrics_builder = metrics_accumulator.entry(id).or_default(); | ||
match metric { | ||
AuthEventMetrics::ThreeDsSdkCount => metrics_builder | ||
.three_ds_sdk_count | ||
.add_metrics_bucket(&value), | ||
AuthEventMetrics::AuthenticationAttemptCount => metrics_builder | ||
.authentication_attempt_count | ||
.add_metrics_bucket(&value), | ||
AuthEventMetrics::AuthenticationSuccessCount => metrics_builder | ||
.authentication_success_count | ||
.add_metrics_bucket(&value), | ||
AuthEventMetrics::ChallengeFlowCount => metrics_builder | ||
.challenge_flow_count | ||
.add_metrics_bucket(&value), | ||
AuthEventMetrics::ChallengeAttemptCount => metrics_builder | ||
.challenge_attempt_count | ||
.add_metrics_bucket(&value), | ||
AuthEventMetrics::ChallengeSuccessCount => metrics_builder | ||
.challenge_success_count | ||
.add_metrics_bucket(&value), | ||
AuthEventMetrics::FrictionlessFlowCount => metrics_builder | ||
.frictionless_flow_count | ||
.add_metrics_bucket(&value), | ||
} | ||
} | ||
} | ||
|
||
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator | ||
.into_iter() | ||
.map(|(id, val)| MetricsBucketResponse { | ||
values: val.collect(), | ||
dimensions: id, | ||
}) | ||
.collect(); | ||
|
||
Ok(MetricsResponse { | ||
query_data, | ||
meta_data: [AnalyticsMetadata { | ||
current_time_range: req.time_range, | ||
}], | ||
}) | ||
} else { | ||
logger::error!("Publishable key not present for merchant ID"); | ||
Ok(MetricsResponse { | ||
query_data: vec![], | ||
meta_data: [AnalyticsMetadata { | ||
current_time_range: req.time_range, | ||
}], | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
use api_models::analytics::{ | ||
auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier}, | ||
Granularity, TimeRange, | ||
}; | ||
use time::PrimitiveDateTime; | ||
|
||
use crate::{ | ||
query::{Aggregate, GroupByClause, ToSql, Window}, | ||
types::{AnalyticsCollection, AnalyticsDataSource, LoadRow, MetricsResult}, | ||
}; | ||
|
||
mod authentication_attempt_count; | ||
mod authentication_success_count; | ||
mod challenge_attempt_count; | ||
mod challenge_flow_count; | ||
mod challenge_success_count; | ||
mod frictionless_flow_count; | ||
mod three_ds_sdk_count; | ||
|
||
use authentication_attempt_count::AuthenticationAttemptCount; | ||
use authentication_success_count::AuthenticationSuccessCount; | ||
use challenge_attempt_count::ChallengeAttemptCount; | ||
use challenge_flow_count::ChallengeFlowCount; | ||
use challenge_success_count::ChallengeSuccessCount; | ||
use frictionless_flow_count::FrictionlessFlowCount; | ||
use three_ds_sdk_count::ThreeDsSdkCount; | ||
|
||
#[derive(Debug, PartialEq, Eq, serde::Deserialize)] | ||
pub struct AuthEventMetricRow { | ||
pub count: Option<i64>, | ||
pub time_bucket: Option<String>, | ||
} | ||
|
||
pub trait AuthEventMetricAnalytics: LoadRow<AuthEventMetricRow> {} | ||
|
||
#[async_trait::async_trait] | ||
pub trait AuthEventMetric<T> | ||
where | ||
T: AnalyticsDataSource + AuthEventMetricAnalytics, | ||
{ | ||
async fn load_metrics( | ||
&self, | ||
merchant_id: &str, | ||
publishable_key: &str, | ||
granularity: &Option<Granularity>, | ||
time_range: &TimeRange, | ||
pool: &T, | ||
) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>>; | ||
} | ||
|
||
#[async_trait::async_trait] | ||
impl<T> AuthEventMetric<T> for AuthEventMetrics | ||
where | ||
T: AnalyticsDataSource + AuthEventMetricAnalytics, | ||
PrimitiveDateTime: ToSql<T>, | ||
AnalyticsCollection: ToSql<T>, | ||
Granularity: GroupByClause<T>, | ||
Aggregate<&'static str>: ToSql<T>, | ||
Window<&'static str>: ToSql<T>, | ||
{ | ||
async fn load_metrics( | ||
&self, | ||
merchant_id: &str, | ||
publishable_key: &str, | ||
granularity: &Option<Granularity>, | ||
time_range: &TimeRange, | ||
pool: &T, | ||
) -> MetricsResult<Vec<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> { | ||
match self { | ||
Self::ThreeDsSdkCount => { | ||
ThreeDsSdkCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
Self::AuthenticationAttemptCount => { | ||
AuthenticationAttemptCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
Self::AuthenticationSuccessCount => { | ||
AuthenticationSuccessCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
Self::ChallengeFlowCount => { | ||
ChallengeFlowCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
Self::ChallengeAttemptCount => { | ||
ChallengeAttemptCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
Self::ChallengeSuccessCount => { | ||
ChallengeSuccessCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
Self::FrictionlessFlowCount => { | ||
FrictionlessFlowCount | ||
.load_metrics(merchant_id, publishable_key, granularity, time_range, pool) | ||
.await | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.