Skip to content

Commit eaffb51

Browse files
tsdk02Sandeep Kumar
authored and
Sandeep Kumar
committed
feat(analytics): add new filters, dimensions and metrics for authentication analytics (#7451)
Co-authored-by: Sandeep Kumar <[email protected]>
1 parent ec0718f commit eaffb51

27 files changed

+1177
-81
lines changed

crates/analytics/src/auth_events.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub mod accumulator;
22
mod core;
3+
pub mod filters;
34
pub mod metrics;
5+
pub mod types;
46
pub use accumulator::{AuthEventMetricAccumulator, AuthEventMetricsAccumulator};
57

6-
pub use self::core::get_metrics;
8+
pub use self::core::{get_filters, get_metrics};

crates/analytics/src/auth_events/accumulator.rs

+25
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ use super::metrics::AuthEventMetricRow;
66
pub struct AuthEventMetricsAccumulator {
77
pub authentication_count: CountAccumulator,
88
pub authentication_attempt_count: CountAccumulator,
9+
pub authentication_error_message: AuthenticationErrorMessageAccumulator,
910
pub authentication_success_count: CountAccumulator,
1011
pub challenge_flow_count: CountAccumulator,
1112
pub challenge_attempt_count: CountAccumulator,
1213
pub challenge_success_count: CountAccumulator,
1314
pub frictionless_flow_count: CountAccumulator,
1415
pub frictionless_success_count: CountAccumulator,
16+
pub authentication_funnel: CountAccumulator,
1517
}
1618

1719
#[derive(Debug, Default)]
@@ -20,6 +22,11 @@ pub struct CountAccumulator {
2022
pub count: Option<i64>,
2123
}
2224

25+
#[derive(Debug, Default)]
26+
pub struct AuthenticationErrorMessageAccumulator {
27+
pub count: Option<i64>,
28+
}
29+
2330
pub trait AuthEventMetricAccumulator {
2431
type MetricOutput;
2532

@@ -44,6 +51,22 @@ impl AuthEventMetricAccumulator for CountAccumulator {
4451
}
4552
}
4653

54+
impl AuthEventMetricAccumulator for AuthenticationErrorMessageAccumulator {
55+
type MetricOutput = Option<u64>;
56+
#[inline]
57+
fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow) {
58+
self.count = match (self.count, metrics.count) {
59+
(None, None) => None,
60+
(None, i @ Some(_)) | (i @ Some(_), None) => i,
61+
(Some(a), Some(b)) => Some(a + b),
62+
}
63+
}
64+
#[inline]
65+
fn collect(self) -> Self::MetricOutput {
66+
self.count.and_then(|i| u64::try_from(i).ok())
67+
}
68+
}
69+
4770
impl AuthEventMetricsAccumulator {
4871
pub fn collect(self) -> AuthEventMetricsBucketValue {
4972
AuthEventMetricsBucketValue {
@@ -55,6 +78,8 @@ impl AuthEventMetricsAccumulator {
5578
challenge_success_count: self.challenge_success_count.collect(),
5679
frictionless_flow_count: self.frictionless_flow_count.collect(),
5780
frictionless_success_count: self.frictionless_success_count.collect(),
81+
error_message_count: self.authentication_error_message.collect(),
82+
authentication_funnel: self.authentication_funnel.collect(),
5883
}
5984
}
6085
}

crates/analytics/src/auth_events/core.rs

+93-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
use std::collections::HashMap;
22

33
use api_models::analytics::{
4-
auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier, MetricsBucketResponse},
5-
AnalyticsMetadata, GetAuthEventMetricRequest, MetricsResponse,
4+
auth_events::{
5+
AuthEventDimensions, AuthEventMetrics, AuthEventMetricsBucketIdentifier,
6+
MetricsBucketResponse,
7+
},
8+
AuthEventFilterValue, AuthEventFiltersResponse, AuthEventMetricsResponse,
9+
AuthEventsAnalyticsMetadata, GetAuthEventFilterRequest, GetAuthEventMetricRequest,
610
};
7-
use error_stack::ResultExt;
11+
use error_stack::{report, ResultExt};
812
use router_env::{instrument, tracing};
913

10-
use super::AuthEventMetricsAccumulator;
14+
use super::{
15+
filters::{get_auth_events_filter_for_dimension, AuthEventFilterRow},
16+
AuthEventMetricsAccumulator,
17+
};
1118
use crate::{
1219
auth_events::AuthEventMetricAccumulator,
1320
errors::{AnalyticsError, AnalyticsResult},
@@ -19,7 +26,7 @@ pub async fn get_metrics(
1926
pool: &AnalyticsProvider,
2027
merchant_id: &common_utils::id_type::MerchantId,
2128
req: GetAuthEventMetricRequest,
22-
) -> AnalyticsResult<MetricsResponse<MetricsBucketResponse>> {
29+
) -> AnalyticsResult<AuthEventMetricsResponse<MetricsBucketResponse>> {
2330
let mut metrics_accumulator: HashMap<
2431
AuthEventMetricsBucketIdentifier,
2532
AuthEventMetricsAccumulator,
@@ -34,7 +41,9 @@ pub async fn get_metrics(
3441
let data = pool
3542
.get_auth_event_metrics(
3643
&metric_type,
44+
&req.group_by_names.clone(),
3745
&merchant_id_scoped,
46+
&req.filters,
3847
req.time_series.map(|t| t.granularity),
3948
&req.time_range,
4049
)
@@ -77,22 +86,94 @@ pub async fn get_metrics(
7786
AuthEventMetrics::FrictionlessSuccessCount => metrics_builder
7887
.frictionless_success_count
7988
.add_metrics_bucket(&value),
89+
AuthEventMetrics::AuthenticationErrorMessage => metrics_builder
90+
.authentication_error_message
91+
.add_metrics_bucket(&value),
92+
AuthEventMetrics::AuthenticationFunnel => metrics_builder
93+
.authentication_funnel
94+
.add_metrics_bucket(&value),
8095
}
8196
}
8297
}
8398

99+
let mut total_error_message_count = 0;
84100
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
85101
.into_iter()
86-
.map(|(id, val)| MetricsBucketResponse {
87-
values: val.collect(),
88-
dimensions: id,
102+
.map(|(id, val)| {
103+
let collected_values = val.collect();
104+
if let Some(count) = collected_values.error_message_count {
105+
total_error_message_count += count;
106+
}
107+
MetricsBucketResponse {
108+
values: collected_values,
109+
dimensions: id,
110+
}
89111
})
90112
.collect();
91-
92-
Ok(MetricsResponse {
113+
Ok(AuthEventMetricsResponse {
93114
query_data,
94-
meta_data: [AnalyticsMetadata {
95-
current_time_range: req.time_range,
115+
meta_data: [AuthEventsAnalyticsMetadata {
116+
total_error_message_count: Some(total_error_message_count),
96117
}],
97118
})
98119
}
120+
121+
pub async fn get_filters(
122+
pool: &AnalyticsProvider,
123+
req: GetAuthEventFilterRequest,
124+
merchant_id: &common_utils::id_type::MerchantId,
125+
) -> AnalyticsResult<AuthEventFiltersResponse> {
126+
let mut res = AuthEventFiltersResponse::default();
127+
for dim in req.group_by_names {
128+
let values = match pool {
129+
AnalyticsProvider::Sqlx(_pool) => {
130+
Err(report!(AnalyticsError::UnknownError))
131+
}
132+
AnalyticsProvider::Clickhouse(pool) => {
133+
get_auth_events_filter_for_dimension(dim, merchant_id, &req.time_range, pool)
134+
.await
135+
.map_err(|e| e.change_context(AnalyticsError::UnknownError))
136+
}
137+
AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) | AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => {
138+
let ckh_result = get_auth_events_filter_for_dimension(
139+
dim,
140+
merchant_id,
141+
&req.time_range,
142+
ckh_pool,
143+
)
144+
.await
145+
.map_err(|e| e.change_context(AnalyticsError::UnknownError));
146+
let sqlx_result = get_auth_events_filter_for_dimension(
147+
dim,
148+
merchant_id,
149+
&req.time_range,
150+
sqlx_pool,
151+
)
152+
.await
153+
.map_err(|e| e.change_context(AnalyticsError::UnknownError));
154+
match (&sqlx_result, &ckh_result) {
155+
(Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => {
156+
router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters")
157+
},
158+
_ => {}
159+
};
160+
ckh_result
161+
}
162+
}
163+
.change_context(AnalyticsError::UnknownError)?
164+
.into_iter()
165+
.filter_map(|fil: AuthEventFilterRow| match dim {
166+
AuthEventDimensions::AuthenticationStatus => fil.authentication_status.map(|i| i.as_ref().to_string()),
167+
AuthEventDimensions::TransactionStatus => fil.trans_status.map(|i| i.as_ref().to_string()),
168+
AuthEventDimensions::ErrorMessage => fil.error_message,
169+
AuthEventDimensions::AuthenticationConnector => fil.authentication_connector.map(|i| i.as_ref().to_string()),
170+
AuthEventDimensions::MessageVersion => fil.message_version,
171+
})
172+
.collect::<Vec<String>>();
173+
res.query_data.push(AuthEventFilterValue {
174+
dimension: dim,
175+
values,
176+
})
177+
}
178+
Ok(res)
179+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use api_models::analytics::{auth_events::AuthEventDimensions, Granularity, TimeRange};
2+
use common_utils::errors::ReportSwitchExt;
3+
use diesel_models::enums::{AuthenticationConnectors, AuthenticationStatus, TransactionStatus};
4+
use error_stack::ResultExt;
5+
use time::PrimitiveDateTime;
6+
7+
use crate::{
8+
query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window},
9+
types::{
10+
AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult,
11+
LoadRow,
12+
},
13+
};
14+
15+
pub trait AuthEventFilterAnalytics: LoadRow<AuthEventFilterRow> {}
16+
17+
pub async fn get_auth_events_filter_for_dimension<T>(
18+
dimension: AuthEventDimensions,
19+
merchant_id: &common_utils::id_type::MerchantId,
20+
time_range: &TimeRange,
21+
pool: &T,
22+
) -> FiltersResult<Vec<AuthEventFilterRow>>
23+
where
24+
T: AnalyticsDataSource + AuthEventFilterAnalytics,
25+
PrimitiveDateTime: ToSql<T>,
26+
AnalyticsCollection: ToSql<T>,
27+
Granularity: GroupByClause<T>,
28+
Aggregate<&'static str>: ToSql<T>,
29+
Window<&'static str>: ToSql<T>,
30+
{
31+
let mut query_builder: QueryBuilder<T> =
32+
QueryBuilder::new(AnalyticsCollection::Authentications);
33+
34+
query_builder.add_select_column(dimension).switch()?;
35+
time_range
36+
.set_filter_clause(&mut query_builder)
37+
.attach_printable("Error filtering time range")
38+
.switch()?;
39+
40+
query_builder
41+
.add_filter_clause("merchant_id", merchant_id)
42+
.switch()?;
43+
44+
query_builder.set_distinct();
45+
46+
query_builder
47+
.execute_query::<AuthEventFilterRow, _>(pool)
48+
.await
49+
.change_context(FiltersError::QueryBuildingError)?
50+
.change_context(FiltersError::QueryExecutionFailure)
51+
}
52+
53+
#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)]
54+
pub struct AuthEventFilterRow {
55+
pub authentication_status: Option<DBEnumWrapper<AuthenticationStatus>>,
56+
pub trans_status: Option<DBEnumWrapper<TransactionStatus>>,
57+
pub error_message: Option<String>,
58+
pub authentication_connector: Option<DBEnumWrapper<AuthenticationConnectors>>,
59+
pub message_version: Option<String>,
60+
}

0 commit comments

Comments
 (0)